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' - '**/*.d.ts'
- '**/@types/' - '**/@types/'
- '**/_types.*' - '**/_types.*'
- '**/*.stories.tsx' - '**/*.stories.*'
- '**/*.test.tsx' - '**/*.test.*'
- '.storybook/' - '.storybook/'
- '.jest/' - '.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: push:
branches: branches:
- main - main
- v4
- v3 - v3
tags: tags:
- '**' - '**'
@ -20,7 +19,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16'] node: ['18']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -47,7 +46,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16'] node: ['18']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -82,7 +81,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: '18'
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v3 uses: actions/cache@v3
env: env:
@ -110,7 +109,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
node: ['16'] node: ['18']
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

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

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ coverage
.next .next
.artifacts .artifacts
.vercel .vercel
.swc
repo-metadata.json repo-metadata.json
networks-metadata.json networks-metadata.json
src/@types/subgraph 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 = { const customJestConfig = {
rootDir: '../', rootDir: '../',
// Add more setup options before each test is run // 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 // 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'], moduleDirectories: ['node_modules', '<rootDir>/src'],
testEnvironment: 'jest-environment-jsdom', testEnvironment: 'jsdom',
moduleNameMapper: { moduleNameMapper: {
'^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx', '^.+\\.(svg)$': '<rootDir>/.jest/__mocks__/svgrMock.tsx',
// '^@/components/(.*)$': '<rootDir>/components/$1', '@components/(.*)$': '<rootDir>/src/components/$1',
'@shared(.*)$': '<rootDir>/src/components/@shared/$1', '@shared(.*)$': '<rootDir>/src/components/@shared/$1',
'@hooks/(.*)$': '<rootDir>/src/@hooks/$1', '@hooks/(.*)$': '<rootDir>/src/@hooks/$1',
'@context/(.*)$': '<rootDir>/src/@context/$1', '@context/(.*)$': '<rootDir>/src/@context/$1',
@ -29,8 +29,25 @@ const customJestConfig = {
'!src/**/*.{stories,test}.{ts,tsx}', '!src/**/*.{stories,test}.{ts,tsx}',
'!src/@types/**/*.{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 // https://github.com/vercel/next.js/issues/35634#issuecomment-1115250297
module.exports = createJestConfig(customJestConfig) 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. 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: To start local development:
```bash ```bash
@ -253,7 +259,7 @@ export default function NetworkName(): ReactElement {
const { networkId, isTestnet } = useWeb3() const { networkId, isTestnet } = useWeb3()
const { networksList } = useNetworkMetadata() const { networksList } = useNetworkMetadata()
const networkData = getNetworkDataById(networksList, networkId) const networkData = getNetworkDataById(networksList, networkId)
const networkName = getNetworkDisplayName(networkData, networkId) const networkName = getNetworkDisplayName(networkData)
return ( return (
<> <>

View File

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

View File

@ -29,19 +29,66 @@
}, },
{ {
"name": "files", "name": "files",
"label": "New file", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "prominentHelp": false,
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. Leaving this field empty will not remove the current value.", "type": "tabs",
"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, "prominentHelp": true,
"type": "files" "type": "files",
"required": true
},
{
"value": "arweave",
"title": "Arweave",
"label": "Transaction ID",
"placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd",
"help": "This Transaction ID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
}
],
"sortOptions": false,
"required": true
}, },
{ {
"name": "links", "name": "links",
"label": "New sample file", "label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "prominentHelp": false,
"help": "Please provide a URL to a sample of your dataset file. This file should reveal the data structure of your dataset, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** Leaving this field empty will not remove the current value.", "type": "tabs",
"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, "prominentHelp": true,
"type": "files" "type": "files",
"required": false
}
],
"required": false
}, },
{ {
@ -66,6 +113,13 @@
"type": "tags", "type": "tags",
"placeholder": "e.g. logistics", "placeholder": "e.g. logistics",
"required": false "required": false
},
{
"name": "paymentCollector",
"label": "Payment Collector Address",
"placeholder": "e.g. 0X123ABC...",
"help": "This address will receive the revenue from all sales. More info available in our [docs](https://docs.oceanprotocol.com/core-concepts/datanft-and-datatoken#revenue).",
"required": false
} }
] ]
} }

View File

@ -24,8 +24,7 @@
}, },
"approval": { "approval": {
"tooltips": { "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.", "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."
} }
} }
} }

View File

@ -104,19 +104,65 @@
{ {
"name": "files", "name": "files",
"label": "File", "label": "File",
"placeholder": "e.g. https://file.com/file.json", "prominentHelp": false,
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.** For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ", "type": "tabs",
"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, "prominentHelp": true,
"type": "files", "type": "files",
"required": true "required": true
}, },
{
"value": "arweave",
"title": "Arweave",
"label": "Transaction ID",
"placeholder": "e.g. DBRCL94j3QqdPaUtt4VWRen8rZfJZBb7Ey40iMpXfhtd",
"help": "This Transaction ID will be stored encrypted after publishing.",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
},
{
"value": "url",
"title": "URL",
"label": "File",
"placeholder": "e.g. https://file.com/file.json",
"help": "This URL will be stored encrypted after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**",
"computeHelp": "For a compute dataset, your file should match the file type required by the algorithm, and should not exceed 1 GB in file size. ",
"prominentHelp": true,
"type": "files",
"required": true
}
],
"sortOptions": false,
"required": true
},
{ {
"name": "links", "name": "links",
"label": "Sample file", "label": "Sample file",
"placeholder": "e.g. https://file.com/samplefile.json", "prominentHelp": false,
"help": "This file should reveal the data structure of your dataset, e.g. by including the header and one line of a CSV file. This file URL will be publicly available after publishing. **Please make sure that the endpoint is accessible over the internet and is not protected by a firewall or by credentials.**", "type": "tabs",
"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, "prominentHelp": true,
"type": "files" "type": "files",
"required": false
}
],
"required": false
}, },
{ {
"name": "algorithmPrivacy", "name": "algorithmPrivacy",

View File

@ -16,6 +16,6 @@
], ],
"announcement": "[Lock your OCEAN](https://df.oceandao.org/) to get veOCEAN, earn rewards and curate data.", "announcement": "[Lock your OCEAN](https://df.oceandao.org/) to get veOCEAN, earn rewards and curate data.",
"warning": { "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."
} }
} }

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,6 +30,7 @@ const tokensPriceQuery = gql`
publishMarketFeeAddress publishMarketFeeAddress
publishMarketFeeToken publishMarketFeeToken
publishMarketFeeAmount publishMarketFeeAmount
templateId
orders( orders(
where: { payer: $account } where: { payer: $account }
orderBy: createdTimestamp orderBy: createdTimestamp
@ -84,6 +85,7 @@ const tokenPriceQuery = gql`
id id
symbol symbol
name name
templateId
publishMarketFeeAddress publishMarketFeeAddress
publishMarketFeeToken publishMarketFeeToken
publishMarketFeeAmount publishMarketFeeAmount
@ -160,7 +162,7 @@ function getAccessDetailsFromTokenPrice(
// the last valid order should be the last reuse order tx id if there is one // the last valid order should be the last reuse order tx id if there is one
accessDetails.validOrderTx = reusedOrder?.tx || order?.tx accessDetails.validOrderTx = reusedOrder?.tx || order?.tx
} }
accessDetails.templateId = tokenPrice.templateId
// TODO: fetch order fee from sub query // TODO: fetch order fee from sub query
accessDetails.publisherMarketOrderFee = tokenPrice?.publishMarketFeeAmount accessDetails.publisherMarketOrderFee = tokenPrice?.publishMarketFeeAmount
@ -169,6 +171,7 @@ function getAccessDetailsFromTokenPrice(
const dispenser = tokenPrice.dispensers[0] const dispenser = tokenPrice.dispensers[0]
accessDetails.type = 'free' accessDetails.type = 'free'
accessDetails.addressOrId = dispenser.token.id accessDetails.addressOrId = dispenser.token.id
accessDetails.price = '0' accessDetails.price = '0'
accessDetails.isPurchasable = dispenser.active accessDetails.isPurchasable = dispenser.active
accessDetails.datatoken = { accessDetails.datatoken = {
@ -323,7 +326,7 @@ export async function getAccessDetailsForAssets(
}, },
queryContext queryContext
) )
tokenQueryResult.data?.tokens.forEach((token) => { tokenQueryResult?.data?.tokens?.forEach((token) => {
const currentAsset = assetsExtended.find( const currentAsset = assetsExtended.find(
(asset) => (asset) =>
asset.services[0].datatokenAddress.toLowerCase() === token.id 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 { 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 axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_orders as OrdersData } from '../@types/subgraph/OrdersData' import { OrdersData_orders as OrdersData } from '../../@types/subgraph/OrdersData'
import { metadataCacheUri } from '../../app.config' import { metadataCacheUri } from '../../../app.config'
import { import {
SortDirectionOptions, SortDirectionOptions,
SortTermOptions SortTermOptions
} from '../@types/aquarius/SearchQuery' } from '../../@types/aquarius/SearchQuery'
import { transformAssetToAssetSelection } from './assetConvertor' import { transformAssetToAssetSelection } from '../assetConvertor'
export interface UserSales { export interface UserSales {
id: string id: string
@ -19,7 +19,7 @@ export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
export function escapeEsReservedCharacters(value: string): string { export function escapeEsReservedCharacters(value: string): string {
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const pattern = /([\!\*\+\-\=\<\>\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g const pattern = /([\!\*\+\-\=\<\>\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g
return value.replace(pattern, '\\$1') return value?.replace(pattern, '\\$1')
} }
/** /**
@ -42,6 +42,24 @@ export function getFilterTerm(
export function generateBaseQuery( export function generateBaseQuery(
baseQueryParams: BaseQueryParams baseQueryParams: BaseQueryParams
): SearchQuery { ): 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 = { const generatedQuery = {
from: baseQueryParams.esPaginationOptions?.from || 0, from: baseQueryParams.esPaginationOptions?.from || 0,
size: size:
@ -51,16 +69,7 @@ export function generateBaseQuery(
query: { query: {
bool: { bool: {
...baseQueryParams.nestedQuery, ...baseQueryParams.nestedQuery,
filter: [ filter: filters
...(baseQueryParams.filters || []),
baseQueryParams.chainIds
? getFilterTerm('chainId', baseQueryParams.chainIds)
: [],
getFilterTerm('_index', 'aquarius'),
...(baseQueryParams.ignorePurgatory
? []
: [getFilterTerm('purgatory.state', false)])
]
} }
} }
} as SearchQuery } as SearchQuery
@ -75,7 +84,6 @@ export function generateBaseQuery(
baseQueryParams.sortOptions.sortDirection || baseQueryParams.sortOptions.sortDirection ||
SortDirectionOptions.Descending SortDirectionOptions.Descending
} }
return generatedQuery return generatedQuery
} }
@ -128,7 +136,7 @@ export async function queryMetadata(
} }
} }
export async function retrieveAsset( export async function getAsset(
did: string, did: string,
cancelToken: CancelToken cancelToken: CancelToken
): Promise<Asset> { ): Promise<Asset> {
@ -171,73 +179,7 @@ export async function getAssetsNames(
} }
} }
export async function getAssetsFromDidList( export async function getAssetsFromDids(
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(
didList: string[], didList: string[],
chainIds: number[], chainIds: number[],
cancelToken: CancelToken cancelToken: CancelToken
@ -276,7 +218,7 @@ export async function getAlgorithmDatasetsForCompute(
must: { must: {
match: { match: {
'services.compute.publisherTrustedAlgorithms.did': { 'services.compute.publisherTrustedAlgorithms.did': {
query: escapeEsReservedCharacters(algorithmId) query: algorithmId
} }
} }
} }
@ -289,7 +231,6 @@ export async function getAlgorithmDatasetsForCompute(
const query = generateBaseQuery(baseQueryParams) const query = generateBaseQuery(baseQueryParams)
const computeDatasets = await queryMetadata(query, cancelToken) const computeDatasets = await queryMetadata(query, cancelToken)
if (computeDatasets?.totalResults === 0) return [] if (computeDatasets?.totalResults === 0) return []
const datasets = await transformAssetToAssetSelection( const datasets = await transformAssetToAssetSelection(
@ -304,6 +245,7 @@ export async function getPublishedAssets(
accountId: string, accountId: string,
chainIds: number[], chainIds: number[],
cancelToken: CancelToken, cancelToken: CancelToken,
ignoreState = false,
page?: number, page?: number,
type?: string, type?: string,
accesType?: string accesType?: string
@ -332,6 +274,7 @@ export async function getPublishedAssets(
} }
}, },
ignorePurgatory: true, ignorePurgatory: true,
ignoreState,
esPaginationOptions: { esPaginationOptions: {
from: (Number(page) - 1 || 0) * 9, from: (Number(page) - 1 || 0) * 9,
size: 9 size: 9
@ -352,7 +295,7 @@ export async function getPublishedAssets(
} }
} }
export async function getTopPublishers( async function getTopPublishers(
chainIds: number[], chainIds: number[],
cancelToken: CancelToken, cancelToken: CancelToken,
page?: number, page?: number,
@ -445,14 +388,17 @@ export async function getDownloadAssets(
dtList: string[], dtList: string[],
tokenOrders: OrdersData[], tokenOrders: OrdersData[],
chainIds: number[], chainIds: number[],
cancelToken: CancelToken cancelToken: CancelToken,
ignoreState = false
): Promise<DownloadedAsset[]> { ): Promise<DownloadedAsset[]> {
const baseQueryparams = { const baseQueryparams = {
chainIds, chainIds,
filters: [ filters: [
getFilterTerm('services.datatokenAddress', dtList), getFilterTerm('services.datatokenAddress', dtList),
getFilterTerm('services.type', 'access') getFilterTerm('services.type', 'access')
] ],
ignorePurgatory: true,
ignoreState
} as BaseQueryParams } as BaseQueryParams
const query = generateBaseQuery(baseQueryparams) const query = generateBaseQuery(baseQueryparams)
try { try {

View File

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

View File

@ -17,14 +17,14 @@ import {
queryMetadata, queryMetadata,
getFilterTerm, getFilterTerm,
generateBaseQuery, generateBaseQuery,
retrieveDDOListByDIDs getAssetsFromDids
} from './aquarius' } from './aquarius'
import { fetchDataForMultipleChains } from './subgraph' import { fetchDataForMultipleChains } from './subgraph'
import { getServiceById, getServiceByName } from './ddo' import { getServiceById, getServiceByName } from './ddo'
import { SortTermOptions } from 'src/@types/aquarius/SearchQuery' import { SortTermOptions } from '../@types/aquarius/SearchQuery'
import { AssetSelectionAsset } from '@shared/FormFields/AssetSelection' import { AssetSelectionAsset } from '@shared/FormInput/InputElement/AssetSelection'
import { transformAssetToAssetSelection } from './assetConvertor' import { transformAssetToAssetSelection } from './assetConvertor'
import { ComputeEditForm } from 'src/components/Asset/Edit/_types' import { ComputeEditForm } from '../components/Asset/Edit/_types'
import { getFileDidInfo } from './provider' import { getFileDidInfo } from './provider'
const getComputeOrders = gql` const getComputeOrders = gql`
@ -338,7 +338,7 @@ export async function createTrustedAlgorithmList(
if (!selectedAlgorithms || selectedAlgorithms.length === 0) if (!selectedAlgorithms || selectedAlgorithms.length === 0)
return trustedAlgorithms return trustedAlgorithms
const selectedAssets = await retrieveDDOListByDIDs( const selectedAssets = await getAssetsFromDids(
selectedAlgorithms, selectedAlgorithms,
[assetChainId], [assetChainId],
cancelToken cancelToken
@ -393,31 +393,3 @@ export async function transformComputeFormToServiceComputeOptions(
return privacy 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' 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( export function getServiceByName(
ddo: Asset | DDO, ddo: Asset | DDO,
name: 'access' | 'compute' 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 Web3 from 'web3'
import { TransactionReceipt } from 'web3-core' import { TransactionReceipt } from 'web3-core'
export async function setMinterToPublisher( export async function setMinterToPublisher(
web3: Web3, web3: Web3,
dispenserAddress: string,
datatokenAddress: string, datatokenAddress: string,
accountId: string, accountId: string,
setError: (msg: string) => void setError: (msg: string) => void
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const dispenserInstance = new Dispenser(dispenserAddress, web3)
const status = await dispenserInstance.status(datatokenAddress)
if (!status?.active) return
const datatokenInstance = new Datatoken(web3) const datatokenInstance = new Datatoken(web3)
const response = await datatokenInstance.removeMinter( const response = await datatokenInstance.removeMinter(
@ -20,6 +15,7 @@ export async function setMinterToPublisher(
accountId, accountId,
accountId accountId
) )
if (!response) { if (!response) {
setError('Updating DDO failed.') setError('Updating DDO failed.')
LoggerInstance.error('Failed at cancelMinter') 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', () => { describe('@utils/ens', () => {
jest.setTimeout(10000) jest.setTimeout(10000)
jest.retryTimes(2) 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' 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 // Boolean value that will be true if we are inside a browser, false otherwise
export const isBrowser = typeof window !== 'undefined' export const isBrowser = typeof window !== 'undefined'
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> { export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
const index = arr.indexOf(value) const index = arr.indexOf(value)
if (index > -1) { if (index > -1) {
@ -14,3 +10,10 @@ export function removeItemFromArray<T>(arr: Array<T>, value: T): Array<T> {
} }
return arr return arr
} }
export function sortAssets(items: Asset[], sorted: string[]) {
items.sort(function (a, b) {
return sorted?.indexOf(a.id) - sorted?.indexOf(b.id)
})
return items
}

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

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,15 @@ import {
approve, approve,
approveWei, approveWei,
Datatoken, Datatoken,
Dispenser,
FixedRateExchange,
FreOrderParams, FreOrderParams,
LoggerInstance, LoggerInstance,
OrderParams, OrderParams,
ProviderComputeInitialize, ProviderComputeInitialize,
ProviderFees, ProviderFees,
ProviderInstance ProviderInstance,
ProviderInitialize
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
import Web3 from 'web3' import Web3 from 'web3'
import { getOceanConfig } from './ocean' import { getOceanConfig } from './ocean'
@ -20,6 +23,26 @@ import {
} from '../../app.config' } from '../../app.config'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
async function initializeProvider(
asset: AssetExtended,
accountId: string,
providerFees?: ProviderFees
): Promise<ProviderInitialize> {
if (providerFees) return
try {
const provider = await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId,
asset.services[0].serviceEndpoint
)
return provider
} catch (error) {
LoggerInstance.log('[Initialize Provider] Error:', error)
}
}
/** /**
* @param web3 * @param web3
* @param asset * @param asset
@ -40,15 +63,11 @@ export async function order(
const datatoken = new Datatoken(web3) const datatoken = new Datatoken(web3)
const config = getOceanConfig(asset.chainId) const config = getOceanConfig(asset.chainId)
const initializeData = const initializeData = await initializeProvider(
!providerFees && asset,
(await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId, accountId,
asset.services[0].serviceEndpoint providerFees
)) )
const orderParams = { const orderParams = {
consumer: computeConsumerAddress || accountId, consumer: computeConsumerAddress || accountId,
@ -66,6 +85,55 @@ export async function order(
switch (asset.accessDetails?.type) { switch (asset.accessDetails?.type) {
case 'fixed': { case 'fixed': {
// this assumes all fees are in ocean // this assumes all fees are in ocean
const freParams = {
exchangeContract: config.fixedRateExchangeAddress,
exchangeId: asset.accessDetails.addressOrId,
maxBaseTokenAmount: orderPriceAndFees.price,
baseTokenAddress: asset?.accessDetails?.baseToken?.address,
baseTokenDecimals: asset?.accessDetails?.baseToken?.decimals || 18,
swapMarketFee: consumeMarketFixedSwapFee,
marketFeeAddress
} as FreOrderParams
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( const txApprove = await approve(
web3, web3,
config, config,
@ -82,33 +150,41 @@ export async function order(
if (!txApprove) { if (!txApprove) {
return return
} }
return await datatoken.buyFromFreAndOrder(
const freParams = {
exchangeContract: config.fixedRateExchangeAddress,
exchangeId: asset.accessDetails.addressOrId,
maxBaseTokenAmount: orderPriceAndFees.price,
baseTokenAddress: asset?.accessDetails?.baseToken?.address,
baseTokenDecimals: asset?.accessDetails?.baseToken?.decimals || 18,
swapMarketFee: consumeMarketFixedSwapFee,
marketFeeAddress
} as FreOrderParams
const tx = await datatoken.buyFromFreAndOrder(
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,
accountId, accountId,
orderParams, orderParams,
freParams freParams
) )
}
return tx break
} }
case 'free': { case 'free': {
const tx = await datatoken.buyFromDispenserAndOrder( 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, asset.services[0].datatokenAddress,
accountId, accountId,
orderParams, orderParams,
config.dispenserAddress config.dispenserAddress
) )
return tx }
} }
} }
} }
@ -130,15 +206,11 @@ export async function reuseOrder(
providerFees?: ProviderFees providerFees?: ProviderFees
): Promise<TransactionReceipt> { ): Promise<TransactionReceipt> {
const datatoken = new Datatoken(web3) const datatoken = new Datatoken(web3)
const initializeData = const initializeData = await initializeProvider(
!providerFees && asset,
(await ProviderInstance.initialize(
asset.id,
asset.services[0].id,
0,
accountId, accountId,
asset.services[0].serviceEndpoint providerFees
)) )
const tx = await datatoken.reuseOrder( const tx = await datatoken.reuseOrder(
asset.accessDetails.datatoken.address, asset.accessDetails.datatoken.address,

View File

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

View File

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

View File

@ -1,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://') const isAllowedUrlScheme = u.startsWith('http://') || u.startsWith('https://')
return isAllowedUrlScheme ? url : 'about:blank' 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 { AllLockedQuery } from '../../src/@types/subgraph/AllLockedQuery'
import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations' import { OwnAllocationsQuery } from '../../src/@types/subgraph/OwnAllocationsQuery'
import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation' import { NftOwnAllocationQuery } from '../../src/@types/subgraph/NftOwnAllocationQuery'
import { OceanLocked } from 'src/@types/subgraph/OceanLocked' import { OceanLockedQuery } from '../../src/@types/subgraph/OceanLockedQuery'
import { gql, OperationResult } from 'urql' import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph' import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios' import axios from 'axios'
@ -11,12 +11,9 @@ import {
getNetworkType, getNetworkType,
NetworkType NetworkType
} from '@hooks/useNetworkMetadata' } from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from 'app.config'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
const AllLocked = gql` const AllLocked = gql`
query AllLocked { query AllLockedQuery {
veOCEANs(first: 1000) { veOCEANs(first: 1000) {
lockedAmount lockedAmount
} }
@ -24,7 +21,7 @@ const AllLocked = gql`
` `
const OwnAllocations = gql` const OwnAllocations = gql`
query OwnAllocations($address: String) { query OwnAllocationsQuery($address: String) {
veAllocations(where: { allocationUser: $address }) { veAllocations(where: { allocationUser: $address }) {
id id
nftAddress nftAddress
@ -33,7 +30,7 @@ const OwnAllocations = gql`
} }
` `
const NftOwnAllocation = gql` const NftOwnAllocation = gql`
query NftOwnAllocation($address: String, $nftAddress: String) { query NftOwnAllocationQuery($address: String, $nftAddress: String) {
veAllocations( veAllocations(
where: { allocationUser: $address, nftAddress: $nftAddress } where: { allocationUser: $address, nftAddress: $nftAddress }
) { ) {
@ -42,7 +39,7 @@ const NftOwnAllocation = gql`
} }
` `
const OceanLocked = gql` const OceanLocked = gql`
query OceanLocked($address: String) { query OceanLockedQuery($address: ID!) {
veOCEAN(id: $address) { veOCEAN(id: $address) {
id id
lockedAmount lockedAmount
@ -80,6 +77,7 @@ export function getVeChainNetworkIds(assetNetworkIds: number[]): number[] {
}) })
return veNetworkIds return veNetworkIds
} }
export async function getNftOwnAllocation( export async function getNftOwnAllocation(
userAddress: string, userAddress: string,
nftAddress: string, nftAddress: string,
@ -87,7 +85,7 @@ export async function getNftOwnAllocation(
): Promise<number> { ): Promise<number> {
const veNetworkId = getVeChainNetworkId(networkId) const veNetworkId = getVeChainNetworkId(networkId)
const queryContext = getQueryContext(veNetworkId) const queryContext = getQueryContext(veNetworkId)
const fetchedAllocation: OperationResult<NftOwnAllocation, any> = const fetchedAllocation: OperationResult<NftOwnAllocationQuery, any> =
await fetchData( await fetchData(
NftOwnAllocation, NftOwnAllocation,
{ {
@ -115,7 +113,7 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
0 0
) )
const fetchedLocked: OperationResult<AllLocked, any> = await fetchData( const fetchedLocked: OperationResult<AllLockedQuery, any> = await fetchData(
AllLocked, AllLocked,
null, null,
queryContext queryContext
@ -136,7 +134,8 @@ export async function getLocked(
const veNetworkIds = getVeChainNetworkIds(networkIds) const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) { for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i]) const queryContext = getQueryContext(veNetworkIds[i])
const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData( const fetchedLocked: OperationResult<OceanLockedQuery, any> =
await fetchData(
OceanLocked, OceanLocked,
{ address: userAddress.toLowerCase() }, { address: userAddress.toLowerCase() },
queryContext queryContext
@ -157,7 +156,7 @@ export async function getOwnAllocations(
const veNetworkIds = getVeChainNetworkIds(networkIds) const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) { for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i]) const queryContext = getQueryContext(veNetworkIds[i])
const fetchedAllocations: OperationResult<OwnAllocations, any> = const fetchedAllocations: OperationResult<OwnAllocationsQuery, any> =
await fetchData( await fetchData(
OwnAllocations, OwnAllocations,
{ address: userAddress.toLowerCase() }, { address: userAddress.toLowerCase() },
@ -176,17 +175,3 @@ export async function getOwnAllocations(
return allocations 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 = { const newNetworkData = {
chainId: `0x${network.chainId.toString(16)}`, chainId: `0x${network.chainId.toString(16)}`,
chainName: getNetworkDisplayName(network, network.chainId), chainName: getNetworkDisplayName(network),
nativeCurrency: network.nativeCurrency, nativeCurrency: network.nativeCurrency,
rpcUrls: network.rpc, rpcUrls: network.rpc,
blockExplorerUrls 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) const cx = classNames.bind(styles)
export interface AddTokenProps {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}
export default function AddToken({ export default function AddToken({
address, address,
symbol, symbol,
text, text,
className, className,
minimal minimal
}: { }: AddTokenProps): ReactElement {
address: string
symbol: string
text?: string
className?: string
minimal?: boolean
}): ReactElement {
const { web3Provider } = useWeb3() const { web3Provider } = useWeb3()
const styleClasses = cx({ 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 handleAction: () => void
} }
export interface AnnouncementBannerProps {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}
export default function AnnouncementBanner({ export default function AnnouncementBanner({
text, text,
action, action,
state, state,
className className
}: { }: AnnouncementBannerProps): ReactElement {
text: string
action?: AnnouncementAction
state?: 'success' | 'warning' | 'error'
className?: string
}): ReactElement {
const styleClasses = cx({ const styleClasses = cx({
banner: true, banner: true,
error: state === 'error', 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 React, { ReactElement, useEffect, useState } from 'react'
import Pagination from '@shared/Pagination' import Pagination from '@shared/Pagination'
import styles from './index.module.css' import styles from './index.module.css'
import classNames from 'classnames/bind'
import Loader from '@shared/atoms/Loader' import Loader from '@shared/atoms/Loader'
import { useUserPreferences } from '@context/UserPreferences'
import { useIsMounted } from '@hooks/useIsMounted' import { useIsMounted } from '@hooks/useIsMounted'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing' import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { useWeb3 } from '@context/Web3' import { useWeb3 } from '@context/Web3'
const cx = classNames.bind(styles)
function LoaderArea() { function LoaderArea() {
return ( return (
<div className={styles.loaderWrap}> <div className={styles.loaderWrap}>
@ -19,7 +15,7 @@ function LoaderArea() {
) )
} }
declare type AssetListProps = { export declare type AssetListProps = {
assets: AssetExtended[] assets: AssetExtended[]
showPagination: boolean showPagination: boolean
page?: number page?: number
@ -28,6 +24,8 @@ declare type AssetListProps = {
onPageChange?: React.Dispatch<React.SetStateAction<number>> onPageChange?: React.Dispatch<React.SetStateAction<number>>
className?: string className?: string
noPublisher?: boolean noPublisher?: boolean
noDescription?: boolean
noPrice?: boolean
} }
export default function AssetList({ export default function AssetList({
@ -38,16 +36,18 @@ export default function AssetList({
isLoading, isLoading,
onPageChange, onPageChange,
className, className,
noPublisher noPublisher,
noDescription,
noPrice
}: AssetListProps): ReactElement { }: AssetListProps): ReactElement {
const { chainIds } = useUserPreferences()
const { accountId } = useWeb3() const { accountId } = useWeb3()
const [assetsWithPrices, setAssetsWithPrices] = useState<AssetExtended[]>() const [assetsWithPrices, setAssetsWithPrices] =
useState<AssetExtended[]>(assets)
const [loading, setLoading] = useState<boolean>(isLoading) const [loading, setLoading] = useState<boolean>(isLoading)
const isMounted = useIsMounted() const isMounted = useIsMounted()
useEffect(() => { useEffect(() => {
if (!assets) return if (!assets || !assets.length) return
setAssetsWithPrices(assets as AssetExtended[]) setAssetsWithPrices(assets as AssetExtended[])
setLoading(false) setLoading(false)
@ -67,24 +67,21 @@ export default function AssetList({
onPageChange(selected + 1) onPageChange(selected + 1)
} }
const styleClasses = cx({ const styleClasses = `${styles.assetList} ${className || ''}`
assetList: true,
[className]: className
})
return chainIds.length === 0 ? ( return loading ? (
<div className={styleClasses}> <LoaderArea />
<div className={styles.empty}>No network selected</div> ) : (
</div>
) : assetsWithPrices && !loading ? (
<> <>
<div className={styleClasses}> <div className={styleClasses}>
{assetsWithPrices.length > 0 ? ( {assetsWithPrices?.length > 0 ? (
assetsWithPrices.map((assetWithPrice) => ( assetsWithPrices?.map((assetWithPrice) => (
<AssetTeaser <AssetTeaser
asset={assetWithPrice} asset={assetWithPrice}
key={assetWithPrice.id} key={assetWithPrice.id}
noPublisher={noPublisher} 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 Link from 'next/link'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '@utils/aquarius' import { getAssetsNames } from '@utils/aquarius'
import styles from './AssetListTitle.module.css' import styles from './index.module.css'
import axios from 'axios' import axios from 'axios'
import { Asset } from '@oceanprotocol/lib' import { Asset } from '@oceanprotocol/lib'
import { useMarketMetadata } from '@context/MarketMetadata' import { useMarketMetadata } from '@context/MarketMetadata'
@ -41,9 +41,7 @@ export default function AssetListTitle({
return ( return (
<h3 className={styles.title}> <h3 className={styles.title}>
<Link href={`/asset/${did || asset?.id}`}> <Link href={`/asset/${did || asset?.id}`}>{assetTitle}</Link>
<a>{assetTitle}</a>
</Link>
</h3> </h3>
) )
} }

View File

@ -21,7 +21,7 @@
} }
.detailLine { .detailLine {
margin-bottom: calc(var(--spacer) / 2); margin-bottom: calc(var(--spacer) / 4);
} }
.content { .content {
@ -43,8 +43,12 @@
overflow-wrap: break-word; overflow-wrap: break-word;
} }
.price {
margin-top: calc(var(--spacer) / 12);
}
.footer { .footer {
margin-top: calc(var(--spacer) / 4); margin-top: calc(var(--spacer) / 24);
} }
.typeLabel { .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 NetworkName from '@shared/NetworkName'
import styles from './index.module.css' import styles from './index.module.css'
import { getServiceByName } from '@utils/ddo' import { getServiceByName } from '@utils/ddo'
import { formatPrice } from '@shared/Price/PriceUnit'
import { useUserPreferences } from '@context/UserPreferences' import { useUserPreferences } from '@context/UserPreferences'
import { formatNumber } from '@utils/numbers'
declare type AssetTeaserProps = { export declare type AssetTeaserProps = {
asset: AssetExtended asset: AssetExtended
noPublisher?: boolean noPublisher?: boolean
noDescription?: boolean
noPrice?: boolean
} }
export default function AssetTeaser({ export default function AssetTeaser({
asset, asset,
noPublisher noPublisher,
noDescription,
noPrice
}: AssetTeaserProps): ReactElement { }: AssetTeaserProps): ReactElement {
const { name, type, description } = asset.metadata const { name, type, description } = asset.metadata
const { datatokens } = asset const { datatokens } = asset
@ -31,8 +35,7 @@ export default function AssetTeaser({
return ( return (
<article className={`${styles.teaser} ${styles[type]}`}> <article className={`${styles.teaser} ${styles[type]}`}>
<Link href={`/asset/${asset.id}`}> <Link href={`/asset/${asset.id}`} className={styles.link}>
<a className={styles.link}>
<aside className={styles.detailLine}> <aside className={styles.detailLine}>
<AssetType <AssetType
className={styles.typeLabel} className={styles.typeLabel}
@ -42,10 +45,7 @@ export default function AssetTeaser({
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{datatokens[0]?.symbol.substring(0, 9)} {datatokens[0]?.symbol.substring(0, 9)}
</span> </span>
<NetworkName <NetworkName networkId={asset.chainId} className={styles.typeLabel} />
networkId={asset.chainId}
className={styles.typeLabel}
/>
</aside> </aside>
<header className={styles.header}> <header className={styles.header}>
<Dotdotdot tagName="h1" clamp={3} className={styles.title}> <Dotdotdot tagName="h1" clamp={3} className={styles.title}>
@ -53,33 +53,59 @@ export default function AssetTeaser({
</Dotdotdot> </Dotdotdot>
{!noPublisher && <Publisher account={owner} minimal />} {!noPublisher && <Publisher account={owner} minimal />}
</header> </header>
{!noDescription && (
<div className={styles.content}> <div className={styles.content}>
<Dotdotdot tagName="p" clamp={3}> <Dotdotdot tagName="p" clamp={3}>
{removeMarkdown(description?.substring(0, 300) || '')} {removeMarkdown(description?.substring(0, 300) || '')}
</Dotdotdot> </Dotdotdot>
</div> </div>
{isUnsupportedPricing ? ( )}
{!noPrice && (
<div className={styles.price}>
{isUnsupportedPricing || !asset.services.length ? (
<strong>No pricing schema available</strong> <strong>No pricing schema available</strong>
) : ( ) : (
<Price accessDetails={asset.accessDetails} size="small" /> <Price accessDetails={asset.accessDetails} size="small" />
)} )}
</div>
)}
<footer className={styles.footer}> <footer className={styles.footer}>
{allocated && allocated > 0 ? ( {allocated && allocated > 0 ? (
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{allocated < 0 {allocated < 0 ? (
? '' ''
: `${formatPrice(allocated, locale)} veOCEAN`} ) : (
<>
<strong>{formatNumber(allocated, locale, '0')}</strong>{' '}
veOCEAN
</>
)}
</span> </span>
) : null} ) : null}
{orders && orders > 0 ? ( {orders && orders > 0 ? (
<span className={styles.typeLabel}> <span className={styles.typeLabel}>
{orders < 0 {orders < 0 ? (
? 'N/A' 'N/A'
: `${orders} ${orders === 1 ? 'sale' : 'sales'}`} ) : (
<>
<strong>{orders}</strong> {orders === 1 ? 'sale' : 'sales'}
</>
)}
</span>
) : null}
{asset.views && asset.views > 0 ? (
<span className={styles.typeLabel}>
{asset.views < 0 ? (
'N/A'
) : (
<>
<strong>{asset.views}</strong>{' '}
{asset.views === 1 ? 'view' : 'views'}
</>
)}
</span> </span>
) : null} ) : null}
</footer> </footer>
</a>
</Link> </Link>
</article> </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 React, { ReactElement, ReactNode, useEffect, useState } from 'react'
import External from '@images/external.svg' import External from '@images/external.svg'
import classNames from 'classnames/bind'
import { Config } from '@oceanprotocol/lib' import { Config } from '@oceanprotocol/lib'
import styles from './index.module.css' import styles from './index.module.css'
import { getOceanConfig } from '@utils/ocean' import { getOceanConfig } from '@utils/ocean'
const cx = classNames.bind(styles)
export default function ExplorerLink({ export default function ExplorerLink({
networkId, networkId,
path, path,
@ -20,10 +17,6 @@ export default function ExplorerLink({
}): ReactElement { }): ReactElement {
const [url, setUrl] = useState<string>() const [url, setUrl] = useState<string>()
const [oceanConfig, setOceanConfig] = useState<Config>() const [oceanConfig, setOceanConfig] = useState<Config>()
const styleClasses = cx({
link: true,
[className]: className
})
useEffect(() => { useEffect(() => {
if (!networkId) return if (!networkId) return
@ -39,7 +32,7 @@ export default function ExplorerLink({
title={`View on ${oceanConfig?.explorerUri}`} title={`View on ${oceanConfig?.explorerUri}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={styleClasses} className={`${styles.link} ${className || ''}`}
> >
{children} <External /> {children} <External />
</a> </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 ( return (
<ul className={styleClasses}> <ul className={styleClasses}>
{!isLoading && file ? ( {!isLoading ? (
<> <>
{file.contentType || file.contentLength ? ( {file?.contentType || file?.contentLength ? (
<> <>
<li>{cleanupContentType(file.contentType)}</li> <li>{cleanupContentType(file.contentType)}</li>
<li> <li>
@ -40,6 +40,9 @@ export default function FileIcon({
? filesize(Number(file.contentLength)).toString() ? filesize(Number(file.contentLength)).toString()
: ''} : ''}
</li> </li>
<li>
{file.type === 'smartcontract' ? 'smart\ncontract' : file.type}
</li>
</> </>
) : ( ) : (
<li className={styles.empty}>No file info available</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 React, { ReactElement } from 'react'
import styles from './Help.module.css' import styles from './Help.module.css'
import Markdown from '@shared/Markdown' import Markdown from '@shared/Markdown'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const FormHelp = ({ const FormHelp = ({
children, children,
@ -12,12 +9,9 @@ const FormHelp = ({
children: string children: string
className?: string className?: string
}): ReactElement => { }): ReactElement => {
const styleClasses = cx({ return (
help: true, <Markdown className={`${styles.help} ${className || ''}`} text={children} />
[className]: className )
})
return <Markdown className={styleClasses} text={children} />
} }
export default FormHelp export default FormHelp

View File

@ -58,11 +58,11 @@
} }
.radio { .radio {
composes: radio from '@shared/FormInput/InputRadio.module.css'; composes: radio from '@shared/FormInput/InputElement/Radio/index.module.css';
} }
.checkbox { .checkbox {
composes: checkbox from '@shared/FormInput/InputRadio.module.css'; composes: checkbox from '@shared/FormInput/InputElement/Radio/index.module.css';
} }
.title { .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