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

Merge pull request #774 from oceanprotocol/feature/account_page

This commit is contained in:
Matthias Kretschmann 2021-09-22 18:05:17 +02:00 committed by GitHub
commit db5b892e23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2590 additions and 1418 deletions

View File

@ -1 +1,2 @@
/asset/* /asset/index.html 200
/asset/* /asset/index.html 200
/profile/* /profile/index.html 200

View File

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

View File

@ -0,0 +1,4 @@
{
"title": "Account",
"description": "Find the data sets and jobs that you previously accessed."
}

View File

@ -12,8 +12,8 @@
"link": "/publish"
},
{
"name": "History",
"link": "/history"
"name": "Profile",
"link": "/profile"
}
],
"warning": {

View File

@ -33,13 +33,16 @@ exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
const handleClientSideOnly = page.path.match(/^\/asset/)
const handleClientSideOnlyAsset = page.path.match(/^\/asset/)
const handleClientSideOnlyAccount = page.path.match(/^\/profile/)
if (handleClientSideOnly) {
if (handleClientSideOnlyAsset) {
page.matchPath = '/asset/*'
// Update the page.
createPage(page)
} else if (handleClientSideOnlyAccount) {
page.matchPath = '/profile/*'
createPage(page)
}
}

116
package-lock.json generated
View File

@ -55,6 +55,7 @@
"query-string": "^7.0.0",
"react": "^17.0.2",
"react-chartjs-2": "^2.11.2",
"react-clipboard.js": "^2.0.16",
"react-data-table-component": "^6.11.7",
"react-dom": "^17.0.2",
"react-dotdotdot": "^1.3.1",
@ -85,7 +86,6 @@
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",
"@types/chart.js": "^2.9.32",
"@types/classnames": "^2.3.1",
"@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",
@ -10514,14 +10514,13 @@
"moment": "^2.10.2"
}
},
"node_modules/@types/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
"deprecated": "This is a stub types definition. classnames provides its own type definitions, so you do not need this installed.",
"dev": true,
"node_modules/@types/clipboard": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz",
"integrity": "sha512-VwVFUHlneOsWfv/GaaY7Kwk4XasDqkAlyFQtsHxnOw0yyBYWTrlEXtmb9RtC+VFBCdtuOeIXECmELNd5RrKp/g==",
"deprecated": "This is a stub types definition. clipboard provides its own type definitions, so you do not need this installed.",
"dependencies": {
"classnames": "*"
"clipboard": "*"
}
},
"node_modules/@types/common-tags": {
@ -16998,6 +16997,16 @@
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw=="
},
"node_modules/clipboard": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz",
"integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/clipboardy": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
@ -19004,6 +19013,11 @@
"node": ">=0.4.0"
}
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -28778,6 +28792,14 @@
"node": ">= 0.10"
}
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/google-protobuf": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz",
@ -44904,6 +44926,20 @@
"react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/react-clipboard.js": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/react-clipboard.js/-/react-clipboard.js-2.0.16.tgz",
"integrity": "sha512-COwmnbrRbl8y4f/SjtonnJTeBRD03YzsHBL5on8iL/uyjERsMkKC7djtfmns7iRAbzadn/84MdpaqaQ3ITP47g==",
"dependencies": {
"@types/clipboard": "^2.0.1",
"clipboard": "^2.0.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": ">=15.5.0",
"react-dom": ">=15.0.0"
}
},
"node_modules/react-data-table-component": {
"version": "6.11.7",
"resolved": "https://registry.npmjs.org/react-data-table-component/-/react-data-table-component-6.11.7.tgz",
@ -47656,6 +47692,11 @@
"seek-table": "bin/seek-bzip-table"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -51465,6 +51506,11 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/tiny-queue": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.1.tgz",
@ -66963,13 +67009,12 @@
"moment": "^2.10.2"
}
},
"@types/classnames": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.3.1.tgz",
"integrity": "sha512-zeOWb0JGBoVmlQoznvqXbE0tEC/HONsnoUNH19Hc96NFsTAwTXbTqb8FMYkru1F/iqp7a18Ws3nWJvtA1sHD1A==",
"dev": true,
"@types/clipboard": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.7.tgz",
"integrity": "sha512-VwVFUHlneOsWfv/GaaY7Kwk4XasDqkAlyFQtsHxnOw0yyBYWTrlEXtmb9RtC+VFBCdtuOeIXECmELNd5RrKp/g==",
"requires": {
"classnames": "*"
"clipboard": "*"
}
},
"@types/common-tags": {
@ -72373,6 +72418,16 @@
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw=="
},
"clipboard": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz",
"integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"clipboardy": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
@ -73977,6 +74032,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -81674,6 +81734,14 @@
"minimatch": "~3.0.2"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"requires": {
"delegate": "^3.1.2"
}
},
"google-protobuf": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz",
@ -94695,6 +94763,16 @@
"prop-types": "^15.7.2"
}
},
"react-clipboard.js": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/react-clipboard.js/-/react-clipboard.js-2.0.16.tgz",
"integrity": "sha512-COwmnbrRbl8y4f/SjtonnJTeBRD03YzsHBL5on8iL/uyjERsMkKC7djtfmns7iRAbzadn/84MdpaqaQ3ITP47g==",
"requires": {
"@types/clipboard": "^2.0.1",
"clipboard": "^2.0.0",
"prop-types": "^15.5.0"
}
},
"react-data-table-component": {
"version": "6.11.7",
"resolved": "https://registry.npmjs.org/react-data-table-component/-/react-data-table-component-6.11.7.tgz",
@ -96908,6 +96986,11 @@
"commander": "^2.8.1"
}
},
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -100004,6 +100087,11 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tiny-queue": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.1.tgz",

View File

@ -70,6 +70,7 @@
"query-string": "^7.0.0",
"react": "^17.0.2",
"react-chartjs-2": "^2.11.2",
"react-clipboard.js": "^2.0.16",
"react-data-table-component": "^6.11.7",
"react-dom": "^17.0.2",
"react-dotdotdot": "^1.3.1",
@ -100,7 +101,6 @@
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.7",
"@types/chart.js": "^2.9.32",
"@types/classnames": "^2.3.1",
"@types/jest": "^26.0.23",
"@types/loadable__component": "^5.13.1",
"@types/lodash.debounce": "^4.0.3",

View File

@ -0,0 +1,8 @@
.blockies {
width: var(--font-size-large);
height: var(--font-size-large);
border-radius: 50%;
overflow: hidden;
display: inline-block;
vertical-align: middle;
}

View File

@ -0,0 +1,23 @@
import { toDataUrl } from 'ethereum-blockies'
import React, { ReactElement } from 'react'
import styles from './Blockies.module.css'
export default function Blockies({
accountId,
className
}: {
accountId: string
className?: string
}): ReactElement {
if (!accountId) return null
const blockies = toDataUrl(accountId)
return (
<img
className={`${styles.blockies} ${className || ''}`}
src={blockies}
alt="Blockies"
aria-hidden="true"
/>
)
}

View File

@ -0,0 +1,35 @@
.button {
display: inline-block;
margin: 0;
border: 0;
box-shadow: none;
background: none;
padding: 0;
cursor: pointer;
position: relative;
}
.icon {
fill: var(--color-secondary);
transition: 0.15s ease-out;
width: 10px;
height: 10px;
}
.button:hover .icon {
fill: var(--font-color-text);
}
.copied .icon,
.button.copied:hover .icon {
fill: var(--brand-alert-green);
}
.copied::after {
content: 'Copied!';
position: absolute;
top: -150%;
left: -140%;
font-size: var(--font-size-mini);
color: var(--brand-alert-green);
}

View File

@ -0,0 +1,33 @@
import React, { ReactElement, useEffect, useState } from 'react'
import loadable from '@loadable/component'
import styles from './Copy.module.css'
import { ReactComponent as IconCopy } from '../../images/copy.svg'
// lazy load when needed only, as library is a bit big
const Clipboard = loadable(() => import('react-clipboard.js'))
export default function Copy({ text }: { text: string }): ReactElement {
const [isCopied, setIsCopied] = useState(false)
// Clear copy success style after 5 sec.
useEffect(() => {
if (!isCopied) return
const timeout = setTimeout(() => {
setIsCopied(false)
}, 5000)
return () => clearTimeout(timeout)
}, [isCopied])
return (
<Clipboard
data-clipboard-text={text}
button-title="Copy to clipboard"
onSuccess={() => setIsCopied(true)}
className={`${styles.button} ${isCopied ? styles.copied : ''}`}
>
<IconCopy className={styles.icon} />
</Clipboard>
)
}

View File

@ -1,72 +0,0 @@
.profile {
background: var(--background-highlight);
border-radius: var(--border-radius);
padding: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 4);
}
@media (min-width: 40rem) {
.profile {
margin: calc(var(--spacer) / 8);
margin-bottom: calc(var(--spacer) / 4);
}
}
.profile p {
margin-bottom: calc(var(--spacer) / 4);
}
.profile code {
padding: 0;
color: var(--color-secondary);
font-size: var(--font-size-mini);
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
.header {
margin-bottom: calc(var(--spacer) / 4);
text-align: center;
}
.header::after {
content: '';
display: block;
margin: calc(var(--spacer) / 2) auto;
width: 20%;
height: 2px;
background: var(--border-color);
}
.image {
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
display: inline-block;
margin-bottom: calc(var(--spacer) / 4);
border: 1px solid var(--border-color);
box-shadow: 0 6px 17px 0 var(--box-shadow-color);
}
.title {
font-size: var(--font-size-base);
margin-bottom: 0;
}
.description {
font-size: var(--font-size-small);
}
.profile p:last-child {
margin-bottom: 0;
}
.meta {
color: var(--color-secondary);
font-size: var(--font-size-mini);
text-align: right;
margin-left: calc(var(--spacer) / 4);
margin-right: calc(var(--spacer) / 4);
}

View File

@ -1,51 +0,0 @@
import React, { ReactElement } from 'react'
import styles from './ProfileDetails.module.css'
import { Profile } from '../../../models/Profile'
import ExplorerLink from '../ExplorerLink'
import PublisherLinks from './PublisherLinks'
export default function ProfileDetails({
profile,
networkId,
account
}: {
profile: Profile
networkId: number
account: string
}): ReactElement {
return (
<>
<div className={styles.profile}>
<header className={styles.header}>
{profile?.image && (
<div className={styles.image}>
<img src={profile.image} width="48" height="48" />
</div>
)}
<h3 className={styles.title}>
{profile?.emoji} {profile?.name}
</h3>
<ExplorerLink networkId={networkId} path={`address/${account}`}>
<code>{account}</code>
</ExplorerLink>
</header>
{profile?.description && (
<p className={styles.description}>{profile?.description}</p>
)}
<PublisherLinks links={profile?.links} />
</div>
<div className={styles.meta}>
Profile data from{' '}
<a
href={`https://www.3box.io/${account}`}
target="_blank"
rel="noreferrer"
>
3Box Hub
</a>
</div>
</>
)
}

View File

@ -8,40 +8,9 @@
}
}
.links {
display: inline;
}
.links a,
.links span {
margin-left: calc(var(--spacer) / 3);
font-size: var(--font-size-mini);
}
.links a:first-child,
.links span:first-child {
margin-left: 0;
}
.links a:hover,
.links a:focus {
color: var(--brand-pink);
}
.linksExternal {
width: 6px;
height: 6px;
display: inline-block;
fill: var(--color-secondary);
}
.detailsTrigger {
cursor: help;
}
.detailsTrigger svg {
width: 10px;
height: 10px;
position: relative;
bottom: -1px;
}

View File

@ -1,17 +1,14 @@
import React, { ReactElement, useEffect, useState } from 'react'
import styles from './index.module.css'
import classNames from 'classnames/bind'
import Tooltip from '../Tooltip'
import { Profile } from '../../../models/Profile'
import { Link } from 'gatsby'
import get3BoxProfile from '../../../utils/profile'
import ExplorerLink from '../ExplorerLink'
import { accountTruncate } from '../../../utils/web3'
import axios from 'axios'
import { ReactComponent as Info } from '../../../images/info.svg'
import ProfileDetails from './ProfileDetails'
import Add from './Add'
import { useWeb3 } from '../../../providers/Web3'
import { getEnsName } from '../../../utils/ens'
const cx = classNames.bind(styles)
@ -24,27 +21,34 @@ export default function Publisher({
minimal?: boolean
className?: string
}): ReactElement {
const { networkId, accountId } = useWeb3()
const { accountId } = useWeb3()
const [profile, setProfile] = useState<Profile>()
const [name, setName] = useState<string>()
const [name, setName] = useState(accountTruncate(account))
const [accountEns, setAccountEns] = useState<string>()
const showAdd = account === accountId && !profile
useEffect(() => {
if (!account) return
setName(accountTruncate(account))
const source = axios.CancelToken.source()
async function get3Box() {
async function getExternalName() {
// ENS
const accountEns = await getEnsName(account)
if (accountEns) {
setAccountEns(accountEns)
setName(accountEns)
}
// 3box
const profile = await get3BoxProfile(account, source.token)
if (!profile) return
setProfile(profile)
const { name, emoji } = profile
name && setName(`${emoji || ''} ${name}`)
}
get3Box()
getExternalName()
return () => {
source.cancel()
@ -63,34 +67,12 @@ export default function Publisher({
) : (
<>
<Link
to={`/search?sort=created&sortOrder=desc&text=${account}`}
title="Show all data sets created by this account."
to={`/profile/${accountEns || account}`}
title="Show profile page."
>
{name}
</Link>
<div className={styles.links}>
{' — '}
{profile && (
<Tooltip
placement="bottom"
content={
<ProfileDetails
profile={profile}
networkId={networkId}
account={account}
/>
}
>
<span className={styles.detailsTrigger}>
Profile <Info className={styles.linksExternal} />
</span>
</Tooltip>
)}
{showAdd && <Add />}
<ExplorerLink networkId={networkId} path={`address/${account}`}>
Explorer
</ExplorerLink>
</div>
{showAdd && <Add />}
</>
)}
</div>

View File

@ -1,8 +1,7 @@
.tabList {
text-align: center;
border-bottom: 1px solid var(--border-color);
padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 2);
padding: calc(var(--spacer) / 2);
}
.tab {
@ -36,5 +35,11 @@
}
.tabContent {
padding: var(--spacer);
padding: calc(var(--spacer) / 2);
}
@media (min-width: 40rem) {
.tabContent {
padding: var(--spacer);
}
}

View File

@ -9,6 +9,10 @@
font-size: var(--font-size-small);
}
.content p {
margin: 0;
}
.icon {
width: 1em;
height: 1em;

View File

@ -5,6 +5,7 @@ import { useSpring, animated } from 'react-spring'
import styles from './Tooltip.module.css'
import { ReactComponent as Info } from '../../images/info.svg'
import { Placement } from 'tippy.js'
import Markdown from './Markdown'
const cx = classNames.bind(styles)

View File

@ -1,5 +1,4 @@
import { DDO } from '@oceanprotocol/lib'
import { useOcean } from '../../providers/Ocean'
import { Link } from 'gatsby'
import React, { ReactElement, useEffect, useState } from 'react'
import { getAssetsNames } from '../../utils/aquarius'
@ -43,7 +42,7 @@ export default function AssetListTitle({
return (
<h3 className={styles.title}>
<Link to={`/asset/${did || ddo.id}`}>{assetTitle}</Link>
<Link to={`/asset/${did || ddo?.id}`}>{assetTitle}</Link>
</h3>
)
}

View File

@ -13,11 +13,13 @@ import { BestPrice } from '../../models/BestPrice'
declare type AssetTeaserProps = {
ddo: DDO
price: BestPrice
noPublisher?: boolean
}
const AssetTeaser: React.FC<AssetTeaserProps> = ({
ddo,
price
price,
noPublisher
}: AssetTeaserProps) => {
const { attributes } = ddo.findServiceByType('metadata')
const { name, type } = attributes.main
@ -34,7 +36,9 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
<Dotdotdot clamp={3}>
<h1 className={styles.title}>{name}</h1>
</Dotdotdot>
<Publisher account={owner} minimal className={styles.publisher} />
{!noPublisher && (
<Publisher account={owner} minimal className={styles.publisher} />
)}
</header>
<AssetType

View File

@ -5,10 +5,7 @@ import { Logger } from '@oceanprotocol/lib'
import Price from '../atoms/Price'
import Tooltip from '../atoms/Tooltip'
import AssetTitle from './AssetListTitle'
import {
queryMetadata,
transformChainIdsListToQuery
} from '../../utils/aquarius'
import { getAssetsFromDidList } from '../../utils/aquarius'
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
import axios, { CancelToken } from 'axios'
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
@ -18,31 +15,8 @@ async function getAssetsBookmarked(
chainIds: number[],
cancelToken: CancelToken
) {
const searchDids = JSON.stringify(bookmarks)
.replace(/,/g, ' ')
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
// for whatever reason ddo.id is not searchable, so use ddo.dataToken instead
.replace(/(did:op:)/g, '0x')
const queryBookmarks = {
page: 1,
offset: 100,
query: {
query_string: {
query: `(${searchDids}) AND (${transformChainIdsListToQuery(
chainIds
)})`,
fields: ['dataToken'],
default_operator: 'OR'
}
},
sort: { created: -1 }
}
try {
const result = await queryMetadata(queryBookmarks, cancelToken)
const result = await getAssetsFromDidList(bookmarks, chainIds, cancelToken)
return result
} catch (error) {
Logger.error(error.message)
@ -88,7 +62,7 @@ export default function Bookmarks(): ReactElement {
const { chainIds } = useUserPreferences()
useEffect(() => {
if (!appConfig.metadataCacheUri || bookmarks === []) return
if (!appConfig?.metadataCacheUri || bookmarks === []) return
const source = axios.CancelToken.source()
@ -121,7 +95,7 @@ export default function Bookmarks(): ReactElement {
return () => {
source.cancel()
}
}, [bookmarks, chainIds])
}, [bookmarks, chainIds, appConfig?.metadataCacheUri])
return (
<Table

View File

@ -0,0 +1,55 @@
.number,
.number * {
font-weight: var(--font-weight-bold);
font-size: var(--font-size-h4);
color: var(--font-color-heading);
margin-left: 0;
}
.number {
white-space: nowrap;
display: inline-flex;
align-items: center;
line-height: 1;
}
.number.small,
.number.small * {
font-size: var(--font-size-h5);
}
.label {
display: block;
}
.number svg {
width: var(--font-size-large);
height: var(--font-size-large);
margin-right: calc(var(--spacer) / 4);
stroke: currentColor;
}
.unit {
font-size: var(--font-size-small);
color: var(--color-secondary);
}
.unit a {
color: var(--color-secondary);
display: block;
padding: 0.5rem 1rem;
border: 0.1rem solid transparent;
border-radius: 0.2rem;
}
.unit a:hover,
.unit a:focus {
background: var(--brand-white);
border: 0.1rem solid var(--brand-pink);
}
.tooltip svg {
width: 0.8em !important;
height: 0.8em !important;
margin-left: 0;
}

View File

@ -0,0 +1,38 @@
import React, { ReactElement } from 'react'
import Markdown from '../atoms/Markdown'
import Tooltip from '../atoms/Tooltip'
import styles from './NumberUnit.module.css'
interface NumberUnitProps {
label: string
value: number | string | Element | ReactElement
small?: boolean
icon?: Element | ReactElement
tooltip?: string
}
export default function NumberUnit({
small,
label,
value,
icon,
tooltip
}: NumberUnitProps): ReactElement {
return (
<div className={styles.unit}>
<div className={`${styles.number} ${small && styles.small}`}>
{icon && icon}
{value}
</div>
<span className={styles.label}>
{label}{' '}
{tooltip && (
<Tooltip
content={<Markdown text={tooltip} />}
className={styles.tooltip}
/>
)}
</span>
</div>
)
}

View File

@ -1,28 +1,23 @@
import React, { useState, useEffect, ReactElement } from 'react'
import { Datatoken, PoolTransaction } from '.'
import { PoolTransaction } from '.'
import { useUserPreferences } from '../../../providers/UserPreferences'
import ExplorerLink from '../../atoms/ExplorerLink'
import { formatPrice } from '../../atoms/Price/PriceUnit'
import styles from './Title.module.css'
function getSymbol(tokenId: Datatoken) {
const symbol = tokenId === null ? 'OCEAN' : tokenId.symbol
return symbol
}
async function getTitle(row: PoolTransaction, locale: string) {
let title = ''
switch (row.event) {
case 'swap': {
const inToken = row.tokens.filter((x) => x.type === 'in')[0]
const inTokenSymbol = getSymbol(inToken.poolToken.tokenId)
const inTokenSymbol = inToken?.poolToken.symbol
const outToken = row.tokens.filter((x) => x.type === 'out')[0]
const outTokenSymbol = getSymbol(outToken.poolToken.tokenId)
const outTokenSymbol = outToken?.poolToken.symbol
title += `Swap ${formatPrice(
Math.abs(inToken.value).toString(),
Math.abs(inToken?.value).toString(),
locale
)}${inTokenSymbol} for ${formatPrice(
Math.abs(outToken.value).toString(),
Math.abs(outToken?.value).toString(),
locale
)}${outTokenSymbol}`
@ -34,18 +29,18 @@ async function getTitle(row: PoolTransaction, locale: string) {
x.tokenAddress.toLowerCase() !==
row.poolAddress.datatokenAddress.toLowerCase()
)[0]
const firstTokenSymbol = await getSymbol(firstToken.poolToken.tokenId)
const firstTokenSymbol = firstToken?.poolToken.symbol
const secondToken = row.tokens.filter(
(x) =>
x.tokenAddress.toLowerCase() ===
row.poolAddress.datatokenAddress.toLowerCase()
)[0]
const secondTokenSymbol = await getSymbol(secondToken.poolToken.tokenId)
const secondTokenSymbol = secondToken?.poolToken.symbol
title += `Create pool with ${formatPrice(
Math.abs(firstToken.value).toString(),
Math.abs(firstToken?.value).toString(),
locale
)}${firstTokenSymbol} and ${formatPrice(
Math.abs(secondToken.value).toString(),
Math.abs(secondToken?.value).toString(),
locale
)}${secondTokenSymbol}`
break
@ -53,7 +48,7 @@ async function getTitle(row: PoolTransaction, locale: string) {
case 'join':
case 'exit': {
for (let i = 0; i < row.tokens.length; i++) {
const tokenSymbol = await getSymbol(row.tokens[i].poolToken.tokenId)
const tokenSymbol = row.tokens[i].poolToken.symbol
if (i > 0) title += '\n'
title += `${row.event === 'join' ? 'Add' : 'Remove'} ${formatPrice(
Math.abs(row.tokens[i].value).toString(),
@ -73,6 +68,7 @@ export default function Title({ row }: { row: PoolTransaction }): ReactElement {
useEffect(() => {
if (!locale || !row) return
async function init() {
const title = await getTitle(row, locale)
setTitle(title)

View File

@ -1,4 +1,4 @@
import React, { ReactElement, useEffect, useState } from 'react'
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import Time from '../../atoms/Time'
import Table from '../../atoms/Table'
import AssetTitle from '../AssetListTitle'
@ -6,14 +6,14 @@ import { useUserPreferences } from '../../../providers/UserPreferences'
import { gql } from 'urql'
import { TransactionHistory_poolTransactions as TransactionHistoryPoolTransactions } from '../../../@types/apollo/TransactionHistory'
import web3 from 'web3'
import { useWeb3 } from '../../../providers/Web3'
import { fetchDataForMultipleChains } from '../../../utils/subgraph'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import NetworkName from '../../atoms/NetworkName'
import { retrieveDDO } from '../../../utils/aquarius'
import axios from 'axios'
import axios, { CancelToken } from 'axios'
import Title from './Title'
import styles from './index.module.css'
import { DDO, Logger } from '@oceanprotocol/lib'
const REFETCH_INTERVAL = 20000
@ -27,10 +27,12 @@ const txHistoryQueryByPool = gql`
) {
tokens {
poolToken {
tokenId {
symbol
}
id
symbol
}
value
type
tokenAddress
}
tx
event
@ -38,11 +40,6 @@ const txHistoryQueryByPool = gql`
poolAddress {
datatokenAddress
}
tokens {
value
type
tokenAddress
}
}
}
`
@ -56,10 +53,12 @@ const txHistoryQuery = gql`
) {
tokens {
poolToken {
tokenId {
symbol
}
id
symbol
}
value
type
tokenAddress
}
tx
event
@ -67,11 +66,6 @@ const txHistoryQuery = gql`
poolAddress {
datatokenAddress
}
tokens {
value
type
tokenAddress
}
}
}
`
@ -82,6 +76,7 @@ export interface Datatoken {
export interface PoolTransaction extends TransactionHistoryPoolTransactions {
networkId: number
ddo: DDO
}
const columns = [
@ -94,11 +89,7 @@ const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: PoolTransaction) {
const did = web3.utils
.toChecksumAddress(row.poolAddress.datatokenAddress)
.replace('0x', 'did:op:')
return <AssetTitle did={did} />
return <AssetTitle ddo={row.ddo} />
}
},
{
@ -130,21 +121,22 @@ const columnsMinimal = [columns[0], columns[3]]
export default function PoolTransactions({
poolAddress,
poolChainId,
minimal
minimal,
accountId
}: {
poolAddress?: string
poolChainId?: number[]
minimal?: boolean
accountId: string
}): ReactElement {
const { accountId } = useWeb3()
const [logs, setLogs] = useState<PoolTransaction[]>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [transactions, setTransactions] = useState<PoolTransaction[]>()
const [isLoading, setIsLoading] = useState<boolean>(true)
const { chainIds } = useUserPreferences()
const { appConfig } = useSiteMetadata()
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
const [data, setData] = useState<PoolTransaction[]>()
async function fetchPoolTransactionData() {
const getPoolTransactionData = useCallback(async () => {
const variables = {
user: accountId?.toLowerCase(),
pool: poolAddress?.toLowerCase()
@ -165,71 +157,94 @@ export default function PoolTransactions({
if (JSON.stringify(data) !== JSON.stringify(transactions)) {
setData(transactions)
}
}
}, [accountId, chainIds, data, poolAddress, poolChainId])
function refetchPoolTransactions() {
if (!dataFetchInterval) {
setDataFetchInterval(
setInterval(function () {
fetchPoolTransactionData()
}, REFETCH_INTERVAL)
const getPoolTransactions = useCallback(
async (cancelToken: CancelToken) => {
if (!data) return
const poolTransactions: PoolTransaction[] = []
for (let i = 0; i < data.length; i++) {
const { datatokenAddress } = data[i].poolAddress
const did = web3.utils
.toChecksumAddress(datatokenAddress)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, cancelToken)
ddo &&
poolTransactions.push({
...data[i],
networkId: ddo?.chainId,
ddo
})
}
const sortedTransactions = poolTransactions.sort(
(a, b) => b.timestamp - a.timestamp
)
}
}
setTransactions(sortedTransactions)
},
[data]
)
//
// Get data, periodically
//
useEffect(() => {
if (!appConfig?.metadataCacheUri) return
async function getTransactions() {
try {
await getPoolTransactionData()
if (dataFetchInterval) return
const interval = setInterval(async () => {
await getPoolTransactionData()
}, REFETCH_INTERVAL)
setDataFetchInterval(interval)
} catch (error) {
Logger.error('Error fetching pool transactions: ', error.message)
}
}
getTransactions()
return () => {
clearInterval(dataFetchInterval)
}
}, [dataFetchInterval])
}, [getPoolTransactionData, dataFetchInterval, appConfig.metadataCacheUri])
//
// Transform to final transactions
//
useEffect(() => {
if (!appConfig.metadataCacheUri) return
const cancelTokenSource = axios.CancelToken.source()
async function getTransactions() {
const poolTransactions: PoolTransaction[] = []
const source = axios.CancelToken.source()
async function transformData() {
try {
setIsLoading(true)
if (!data) {
await fetchPoolTransactionData()
return
}
const poolTransactionsData = data.map((obj) => ({ ...obj }))
for (let i = 0; i < poolTransactionsData.length; i++) {
const did = web3.utils
.toChecksumAddress(
poolTransactionsData[i].poolAddress.datatokenAddress
)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, source.token)
poolTransactionsData[i].networkId = ddo.chainId
poolTransactions.push(poolTransactionsData[i])
}
const sortedTransactions = poolTransactions.sort(
(a, b) => b.timestamp - a.timestamp
)
setLogs(sortedTransactions)
refetchPoolTransactions()
await getPoolTransactions(cancelTokenSource.token)
} catch (error) {
console.error('Error fetching pool transactions: ', error.message)
Logger.error('Error fetching pool transactions: ', error.message)
} finally {
setIsLoading(false)
}
}
getTransactions()
}, [accountId, chainIds, appConfig.metadataCacheUri, poolAddress, data])
transformData()
return () => {
cancelTokenSource.cancel()
}
}, [getPoolTransactions])
return accountId ? (
<Table
columns={minimal ? columnsMinimal : columns}
data={logs}
data={transactions}
isLoading={isLoading}
noTableHead={minimal}
dense={minimal}
pagination={minimal ? logs?.length >= 4 : logs?.length >= 9}
pagination={
minimal ? transactions?.length >= 4 : transactions?.length >= 9
}
paginationPerPage={minimal ? 5 : 10}
/>
) : (

View File

@ -41,20 +41,12 @@
}
}
.blockies {
width: var(--font-size-large);
height: var(--font-size-large);
border-radius: 50%;
overflow: hidden;
display: inline-block;
vertical-align: middle;
}
.address {
display: none;
text-transform: none;
border-right: 1px solid var(--border-color);
padding-right: calc(var(--spacer) / 3);
padding-left: calc(var(--spacer) / 8);
}
@media screen and (min-width: 60rem) {

View File

@ -1,29 +1,15 @@
import { toDataUrl } from 'ethereum-blockies'
import React, { FormEvent } from 'react'
import { ReactComponent as Caret } from '../../../images/caret.svg'
import { accountTruncate } from '../../../utils/web3'
import Loader from '../../atoms/Loader'
import styles from './Account.module.css'
import { useWeb3 } from '../../../providers/Web3'
const Blockies = ({ account }: { account: string | undefined }) => {
if (!account) return null
const blockies = toDataUrl(account)
return (
<img
className={styles.blockies}
src={blockies}
alt="Blockies"
aria-hidden="true"
/>
)
}
import Blockies from '../../atoms/Blockies'
// Forward ref for Tippy.js
// eslint-disable-next-line
const Account = React.forwardRef((props, ref: any) => {
const { accountId, web3Modal, connect } = useWeb3()
const { accountId, accountEns, web3Modal, connect } = useWeb3()
async function handleActivation(e: FormEvent<HTMLButtonElement>) {
// prevent accidentially submitting a form the button might be in
@ -44,9 +30,9 @@ const Account = React.forwardRef((props, ref: any) => {
ref={ref}
onClick={(e) => e.preventDefault()}
>
<Blockies account={accountId} />
<Blockies accountId={accountId} />
<span className={styles.address} title={accountId}>
{accountTruncate(accountId)}
{accountTruncate(accountEns || accountId)}
</span>
<Caret aria-hidden="true" className={styles.caret} />
</button>

View File

@ -38,11 +38,11 @@ import { AssetSelectionAsset } from '../../../molecules/FormFields/AssetSelectio
import AlgorithmDatasetsListForCompute from '../../AssetContent/AlgorithmDatasetsListForCompute'
import { getPreviousOrders, getPrice } from '../../../../utils/subgraph'
import AssetActionHistoryTable from '../../AssetActionHistoryTable'
import ComputeJobs from '../../../pages/History/ComputeJobs'
import ComputeJobs from '../../../pages/Profile/History/ComputeJobs'
import { BestPrice } from '../../../../models/BestPrice'
const SuccessAction = () => (
<Button style="text" to="/history?defaultTab=ComputeJobs" size="small">
<Button style="text" to="/profile?defaultTab=ComputeJobs" size="small">
Go to history
</Button>
)
@ -473,11 +473,7 @@ export default function Compute({
</footer>
{accountId && (
<AssetActionHistoryTable title="Your Compute Jobs">
<ComputeJobs
minimal
assetDTAddress={ddo.dataTokenInfo.address}
chainId={ddo.chainId}
/>
<ComputeJobs minimal />
</AssetActionHistoryTable>
)}
</>

View File

@ -515,6 +515,7 @@ export default function Pool(): ReactElement {
{accountId && (
<AssetActionHistoryTable title="Your Pool Transactions">
<PoolTransactions
accountId={accountId}
poolAddress={price?.address}
poolChainId={[ddo.chainId]}
minimal

View File

@ -26,6 +26,7 @@ declare type AssetListProps = {
isLoading?: boolean
onPageChange?: React.Dispatch<React.SetStateAction<number>>
className?: string
noPublisher?: boolean
}
const AssetList: React.FC<AssetListProps> = ({
@ -35,7 +36,8 @@ const AssetList: React.FC<AssetListProps> = ({
totalPages,
isLoading,
onPageChange,
className
className,
noPublisher
}) => {
const { chainIds } = useUserPreferences()
const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>()
@ -71,6 +73,7 @@ const AssetList: React.FC<AssetListProps> = ({
ddo={assetWithPrice.ddo}
price={assetWithPrice.price}
key={assetWithPrice.ddo.id}
noPublisher={noPublisher}
/>
))
) : chainIds.length === 0 ? (

View File

@ -1,358 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import web3 from 'web3'
import Time from '../../../atoms/Time'
import { Link } from 'gatsby'
import { DDO, Logger, Service, Provider } from '@oceanprotocol/lib'
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
import Dotdotdot from 'react-dotdotdot'
import Table from '../../../atoms/Table'
import Button from '../../../atoms/Button'
import { useOcean } from '../../../../providers/Ocean'
import { gql } from 'urql'
import { useWeb3 } from '../../../../providers/Web3'
import {
queryMetadata,
transformChainIdsListToQuery
} from '../../../../utils/aquarius'
import axios, { CancelToken } from 'axios'
import Details from './Details'
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import { ReactComponent as Refresh } from '../../../../images/refresh.svg'
import styles from './index.module.css'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import { getOceanConfig } from '../../../../utils/ocean'
import { fetchDataForMultipleChains } from '../../../../utils/subgraph'
import {
OrdersData_tokenOrders as OrdersData,
OrdersData_tokenOrders_datatokenId as OrdersDatatoken
} from '../../../../@types/apollo/OrdersData'
import NetworkName from '../../../atoms/NetworkName'
const getComputeOrders = gql`
query ComputeOrders($user: String!) {
tokenOrders(
orderBy: timestamp
orderDirection: desc
where: { payer: $user }
) {
id
serviceId
datatokenId {
address
}
tx
timestamp
}
}
`
const getComputeOrdersByDatatokenAddress = gql`
query ComputeOrdersByDatatokenAddress(
$user: String!
$datatokenAddress: String!
) {
tokenOrders(
orderBy: timestamp
orderDirection: desc
where: { payer: $user, datatokenId: $datatokenAddress }
) {
id
serviceId
datatokenId {
address
}
tx
timestamp
}
}
`
interface TokenOrder {
id: string
serviceId: number
datatokenId: OrdersDatatoken
tx: any | null
timestamp: number
}
export function Status({ children }: { children: string }): ReactElement {
return <div className={styles.status}>{children}</div>
}
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: ComputeJobMetaData) {
return (
<Dotdotdot clamp={2}>
<Link to={`/asset/${row.inputDID[0]}`}>{row.assetName}</Link>
</Dotdotdot>
)
}
},
{
name: 'Network',
selector: function getNetwork(row: ComputeJobMetaData) {
return <NetworkName networkId={row.networkId} />
}
},
{
name: 'Created',
selector: function getTimeRow(row: ComputeJobMetaData) {
return <Time date={row.dateCreated} isUnix relative />
}
},
{
name: 'Finished',
selector: function getTimeRow(row: ComputeJobMetaData) {
return row.dateFinished ? (
<Time date={row.dateFinished} isUnix relative />
) : (
''
)
}
},
{
name: 'Status',
selector: function getStatus(row: ComputeJobMetaData) {
return <Status>{row.statusText}</Status>
}
},
{
name: 'Actions',
selector: function getActions(row: ComputeJobMetaData) {
return <Details job={row} />
}
}
]
async function getAssetMetadata(
queryDtList: string,
cancelToken: CancelToken,
chainIds: number[]
): Promise<DDO[]> {
const queryDid = {
page: 1,
offset: 100,
query: {
query_string: {
query: `(${queryDtList}) AND (${transformChainIdsListToQuery(
chainIds
)}) AND service.attributes.main.type:dataset AND service.type:compute`,
fields: ['dataToken']
}
}
}
const result = await queryMetadata(queryDid, cancelToken)
return result.results
}
export default function ComputeJobs({
minimal,
assetDTAddress,
chainId
}: {
minimal?: boolean
assetDTAddress?: string
chainId?: number
}): ReactElement {
const { ocean, account, config, connect } = useOcean()
const { accountId, networkId } = useWeb3()
const [isLoading, setIsLoading] = useState(true)
const { chainIds } = useUserPreferences()
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const columnsMinimal = [columns[4], columns[5], columns[3]]
useEffect(() => {
async function initOcean() {
const oceanInitialConfig = getOceanConfig(networkId)
await connect(oceanInitialConfig)
}
if (ocean === undefined) {
initOcean()
}
}, [networkId, ocean])
async function getJobs() {
if (!accountId) return
setIsLoading(true)
const variables = assetDTAddress
? {
user: accountId?.toLowerCase(),
datatokenAddress: assetDTAddress.toLowerCase()
}
: {
user: accountId?.toLowerCase()
}
const result = await fetchDataForMultipleChains(
assetDTAddress ? getComputeOrdersByDatatokenAddress : getComputeOrders,
variables,
assetDTAddress ? [chainId] : chainIds
)
let data: TokenOrder[] = []
for (let i = 0; i < result.length; i++) {
if (!result[i].tokenOrders) continue
result[i].tokenOrders.forEach((tokenOrder: TokenOrder) => {
data.push(tokenOrder)
})
}
if (!ocean || !account || !data) {
return
}
data = data.sort((a, b) => b.timestamp - a.timestamp)
const dtList = []
const computeJobs: ComputeJobMetaData[] = []
for (let i = 0; i < data.length; i++) {
dtList.push(data[i].datatokenId.address)
}
const queryDtList = JSON.stringify(dtList)
.replace(/,/g, ' ')
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
if (queryDtList === '') {
setJobs([])
setIsLoading(false)
return
}
try {
setIsLoading(true)
const source = axios.CancelToken.source()
const assets = await getAssetMetadata(queryDtList, source.token, chainIds)
const providers: Provider[] = []
const serviceEndpoints: string[] = []
for (let i = 0; i < data.length; i++) {
try {
const did = web3.utils
.toChecksumAddress(data[i].datatokenId.address)
.replace('0x', 'did:op:')
const ddo = assets.filter((x) => x.id === did)[0]
if (ddo === undefined) continue
const service = ddo.service.filter(
(x: Service) => x.index === data[i].serviceId
)[0]
if (!service || service.type !== 'compute') continue
const { serviceEndpoint } = service
const wasProviderQueried =
serviceEndpoints.filter((x) => x === serviceEndpoint).length > 0
if (wasProviderQueried) continue
serviceEndpoints.push(serviceEndpoint)
} catch (err) {
Logger.error(err)
}
}
try {
setIsLoading(true)
for (let i = 0; i < serviceEndpoints.length; i++) {
const instanceConfig = {
config,
web3: config.web3Provider,
logger: Logger,
ocean: ocean
}
const provider = await Provider.getInstance(instanceConfig)
await provider.setBaseUrl(serviceEndpoints[i])
const hasSameCompute =
providers.filter(
(x) => x.computeAddress === provider.computeAddress
).length > 0
if (!hasSameCompute) providers.push(provider)
}
} catch (err) {
Logger.error(err)
}
for (let i = 0; i < providers.length; i++) {
try {
const providerComputeJobs = (await providers[i].computeStatus(
'',
account,
undefined,
undefined,
false
)) as ComputeJob[]
// means the provider uri is not good, so we ignore it and move on
if (!providerComputeJobs) continue
providerComputeJobs.sort((a, b) => {
if (a.dateCreated > b.dateCreated) {
return -1
}
if (a.dateCreated < b.dateCreated) {
return 1
}
return 0
})
for (let j = 0; j < providerComputeJobs.length; j++) {
const job = providerComputeJobs[j]
const did = job.inputDID[0]
const ddo = assets.filter((x) => x.id === did)[0]
if (!ddo) continue
const serviceMetadata = ddo.service.filter(
(x: Service) => x.type === 'metadata'
)[0]
const compJob: ComputeJobMetaData = {
...job,
assetName: serviceMetadata.attributes.main.name,
assetDtSymbol: ddo.dataTokenInfo.symbol,
networkId: ddo.chainId
}
computeJobs.push(compJob)
}
} catch (err) {
Logger.error(err)
}
}
setJobs(computeJobs)
} catch (error) {
Logger.log(error.message)
} finally {
setIsLoading(false)
}
return true
}
useEffect(() => {
if (!chainIds || !accountId) {
setIsLoading(false)
return
}
getJobs()
}, [ocean, account, chainIds, accountId])
return accountId ? (
<>
{jobs.length <= 0 || minimal || (
<Button
style="text"
size="small"
title="Refresh compute jobs"
onClick={() => getJobs()}
disabled={isLoading}
className={styles.refresh}
>
<Refresh />
Refresh
</Button>
)}
<Table
columns={minimal ? columnsMinimal : columns}
data={jobs}
isLoading={isLoading}
defaultSortField="row.dateCreated"
defaultSortAsc={false}
/>
</>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -1,135 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Table from '../../atoms/Table'
import { gql } from 'urql'
import Time from '../../atoms/Time'
import web3 from 'web3'
import AssetTitle from '../../molecules/AssetListTitle'
import { useWeb3 } from '../../../providers/Web3'
import axios from 'axios'
import { retrieveDDO } from '../../../utils/aquarius'
import { Logger } from '@oceanprotocol/lib'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { fetchDataForMultipleChains } from '../../../utils/subgraph'
import { OrdersData_tokenOrders as OrdersData } from '../../../@types/apollo/OrdersData'
import NetworkName from '../../atoms/NetworkName'
const getTokenOrders = gql`
query OrdersData($user: String!) {
tokenOrders(
orderBy: timestamp
orderDirection: desc
where: { consumer: $user }
) {
datatokenId {
address
symbol
}
timestamp
tx
}
}
`
interface DownloadedAssets {
did: string
dtSymbol: string
timestamp: number
networkId: number
}
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: DownloadedAssets) {
return <AssetTitle did={row.did} />
}
},
{
name: 'Network',
selector: function getNetwork(row: DownloadedAssets) {
return <NetworkName networkId={row.networkId} />
}
},
{
name: 'Datatoken',
selector: function getTitleRow(row: DownloadedAssets) {
return row.dtSymbol
}
},
{
name: 'Time',
selector: function getTimeRow(row: DownloadedAssets) {
return <Time date={row.timestamp.toString()} relative isUnix />
}
}
]
export default function ComputeDownloads(): ReactElement {
const { accountId } = useWeb3()
const { appConfig } = useSiteMetadata()
const [isLoading, setIsLoading] = useState(false)
const [orders, setOrders] = useState<DownloadedAssets[]>()
const { chainIds } = useUserPreferences()
useEffect(() => {
const variables = { user: accountId?.toLowerCase() }
async function filterAssets() {
const filteredOrders: DownloadedAssets[] = []
const source = axios.CancelToken.source()
try {
setIsLoading(true)
const response = await fetchDataForMultipleChains(
getTokenOrders,
variables,
chainIds
)
const data: OrdersData[] = []
for (let i = 0; i < response.length; i++) {
response[i].tokenOrders.forEach((tokenOrder: OrdersData) => {
data.push(tokenOrder)
})
}
for (let i = 0; i < data.length; i++) {
const did = web3.utils
.toChecksumAddress(data[i].datatokenId.address)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, source.token)
if (!ddo) continue
if (ddo.service[1].type === 'access') {
filteredOrders.push({
did: did,
networkId: ddo.chainId,
dtSymbol: data[i].datatokenId.symbol,
timestamp: data[i].timestamp
})
}
}
const sortedOrders = filteredOrders.sort(
(a, b) => b.timestamp - a.timestamp
)
setOrders(sortedOrders)
} catch (err) {
Logger.log(err.message)
} finally {
setIsLoading(false)
}
}
filterAssets()
}, [accountId, appConfig.metadataCacheUri, chainIds])
return accountId ? (
<Table
columns={columns}
data={orders}
paginationPerPage={10}
isLoading={isLoading}
/>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -1,269 +0,0 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Table from '../../atoms/Table'
import Conversion from '../../atoms/Price/Conversion'
import styles from './PoolShares.module.css'
import AssetTitle from '../../molecules/AssetListTitle'
import { gql } from 'urql'
import {
PoolShares as PoolSharesList,
PoolShares_poolShares as PoolShare,
PoolShares_poolShares_poolId_tokens as PoolSharePoolIdTokens
} from '../../../@types/apollo/PoolShares'
import web3 from 'web3'
import Token from '../../organisms/AssetActions/Pool/Token'
import { useWeb3 } from '../../../providers/Web3'
import { useUserPreferences } from '../../../providers/UserPreferences'
import { fetchDataForMultipleChains } from '../../../utils/subgraph'
import NetworkName from '../../atoms/NetworkName'
import axios from 'axios'
import { retrieveDDO } from '../../../utils/aquarius'
import { isValidNumber } from './../../../utils/numberValidations'
import Decimal from 'decimal.js'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
const REFETCH_INTERVAL = 20000
const poolSharesQuery = gql`
query PoolShares($user: String) {
poolShares(where: { userAddress: $user, balance_gt: 0.001 }, first: 1000) {
id
balance
userAddress {
id
}
poolId {
id
datatokenAddress
valueLocked
tokens {
id
isDatatoken
symbol
}
oceanReserve
datatokenReserve
totalShares
consumePrice
spotPrice
createTime
}
}
}
`
interface Asset {
userLiquidity: number
poolShare: PoolShare
networkId: number
createTime: number
}
function calculateUserLiquidity(poolShare: PoolShare) {
const ocean =
(poolShare.balance / poolShare.poolId.totalShares) *
poolShare.poolId.oceanReserve
const datatokens =
(poolShare.balance / poolShare.poolId.totalShares) *
poolShare.poolId.datatokenReserve
const totalLiquidity = ocean + datatokens * poolShare.poolId.consumePrice
return totalLiquidity
}
function findTokenByType(tokens: PoolSharePoolIdTokens[], type: string) {
const { symbol } = tokens.find((token) =>
type === 'datatoken'
? token.isDatatoken === true
: token.isDatatoken === false
)
return symbol
}
function Symbol({ tokens }: { tokens: PoolSharePoolIdTokens[] }) {
return <>{findTokenByType(tokens, 'datatoken')}</>
}
function Liquidity({ row, type }: { row: Asset; type: string }) {
let price = ``
let oceanTokenBalance = ''
let dataTokenBalance = ''
if (type === 'user') {
price = `${row.userLiquidity}`
const userShare = row.poolShare.balance / row.poolShare.poolId.totalShares
oceanTokenBalance = (
userShare * row.poolShare.poolId.oceanReserve
).toString()
dataTokenBalance = (
userShare * row.poolShare.poolId.datatokenReserve
).toString()
}
if (type === 'pool') {
price =
isValidNumber(row.poolShare.poolId.oceanReserve) &&
isValidNumber(row.poolShare.poolId.datatokenReserve) &&
isValidNumber(row.poolShare.poolId.consumePrice)
? new Decimal(row.poolShare.poolId.datatokenReserve)
.mul(new Decimal(row.poolShare.poolId.consumePrice))
.plus(row.poolShare.poolId.oceanReserve)
.toString()
: '0'
oceanTokenBalance = row.poolShare.poolId.oceanReserve.toString()
dataTokenBalance = row.poolShare.poolId.datatokenReserve.toString()
}
return (
<div className={styles.yourLiquidity}>
<Conversion
price={price}
className={styles.totalLiquidity}
hideApproximateSymbol
/>
<Token
symbol={findTokenByType(row.poolShare.poolId.tokens, 'ocean')}
balance={oceanTokenBalance}
noIcon
/>
<Token
symbol={findTokenByType(row.poolShare.poolId.tokens, 'datatoken')}
balance={dataTokenBalance}
noIcon
/>
</div>
)
}
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: Asset) {
const did = web3.utils
.toChecksumAddress(row.poolShare.poolId.datatokenAddress)
.replace('0x', 'did:op:')
return <AssetTitle did={did} />
},
grow: 2
},
{
name: 'Network',
selector: function getNetwork(row: Asset) {
return <NetworkName networkId={row.networkId} />
}
},
{
name: 'Datatoken',
selector: function getSymbol(row: Asset) {
return <Symbol tokens={row.poolShare.poolId.tokens} />
}
},
{
name: 'Your Liquidity',
selector: function getAssetRow(row: Asset) {
return <Liquidity row={row} type="user" />
},
right: true
},
{
name: 'Pool Liquidity',
selector: function getAssetRow(row: Asset) {
return <Liquidity row={row} type="pool" />
},
right: true
}
]
export default function PoolShares(): ReactElement {
const { accountId } = useWeb3()
const [assets, setAssets] = useState<Asset[]>()
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<PoolShare[]>()
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
const { chainIds } = useUserPreferences()
async function fetchPoolSharesData() {
const variables = { user: accountId?.toLowerCase() }
const shares: PoolShare[] = []
const result = await fetchDataForMultipleChains(
poolSharesQuery,
variables,
chainIds
)
for (let i = 0; i < result.length; i++) {
result[i].poolShares.forEach((poolShare: PoolShare) => {
shares.push(poolShare)
})
}
if (JSON.stringify(data) !== JSON.stringify(shares)) {
setData(shares)
}
}
function refetchPoolShares() {
if (!dataFetchInterval) {
setDataFetchInterval(
setInterval(function () {
fetchPoolSharesData()
}, REFETCH_INTERVAL)
)
}
}
useEffect(() => {
return () => {
clearInterval(dataFetchInterval)
}
}, [dataFetchInterval])
useEffect(() => {
async function getShares() {
const assetList: Asset[] = []
const source = axios.CancelToken.source()
try {
setLoading(true)
if (!data) {
await fetchPoolSharesData()
return
}
for (let i = 0; i < data.length; i++) {
const did = web3.utils
.toChecksumAddress(data[i].poolId.datatokenAddress)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, source.token)
const userLiquidity = calculateUserLiquidity(data[i])
assetList.push({
poolShare: data[i],
userLiquidity: userLiquidity,
networkId: ddo.chainId,
createTime: data[i].poolId.createTime
})
}
const orderedAssets = assetList.sort(
(a, b) => b.createTime - a.createTime
)
setAssets(orderedAssets)
refetchPoolShares()
} catch (error) {
console.error('Error fetching pool shares: ', error.message)
} finally {
setLoading(false)
}
}
getShares()
}, [accountId, chainIds, data])
return accountId ? (
<Table
columns={columns}
className={styles.poolSharesTable}
data={assets}
pagination
paginationPerPage={5}
isLoading={loading}
sortField="userLiquidity"
sortAsc={false}
/>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -1,67 +0,0 @@
import { Logger } from '@oceanprotocol/lib'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import React, { ReactElement, useEffect, useState } from 'react'
import AssetList from '../../organisms/AssetList'
import axios from 'axios'
import {
queryMetadata,
transformChainIdsListToQuery
} from '../../../utils/aquarius'
import { useWeb3 } from '../../../providers/Web3'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../providers/UserPreferences'
export default function PublishedList(): ReactElement {
const { accountId } = useWeb3()
const { appConfig } = useSiteMetadata()
const { chainIds } = useUserPreferences()
const [queryResult, setQueryResult] = useState<QueryResult>()
const [isLoading, setIsLoading] = useState(false)
const [page, setPage] = useState<number>(1)
useEffect(() => {
async function getPublished() {
if (!accountId) return
const queryPublishedAssets = {
page: page,
offset: 9,
query: {
query_string: {
query: `(publicKey.owner:${accountId}) AND (${transformChainIdsListToQuery(
chainIds
)})`
}
},
sort: { created: -1 }
}
try {
const source = axios.CancelToken.source()
queryResult || setIsLoading(true)
const result = await queryMetadata(queryPublishedAssets, source.token)
setQueryResult(result)
} catch (error) {
Logger.error(error.message)
} finally {
setIsLoading(false)
}
}
getPublished()
}, [accountId, page, appConfig.metadataCacheUri, chainIds])
return accountId ? (
<AssetList
assets={queryResult?.results}
isLoading={isLoading}
showPagination
page={queryResult?.page}
totalPages={queryResult?.totalPages}
onPageChange={(newPage) => {
setPage(newPage)
}}
/>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -1,54 +0,0 @@
import React, { ReactElement } from 'react'
import Tabs from '../../atoms/Tabs'
import PoolShares from './PoolShares'
import PoolTransactions from '../../molecules/PoolTransactions'
import PublishedList from './PublishedList'
import Downloads from './Downloads'
import ComputeJobs from './ComputeJobs'
import styles from './index.module.css'
import { useUserPreferences } from '../../../providers/UserPreferences'
import OceanProvider from '../../../providers/Ocean'
const tabs = [
{
title: 'Published',
content: <PublishedList />
},
{
title: 'Pool Shares',
content: <PoolShares />
},
{
title: 'Pool Transactions',
content: <PoolTransactions />
},
{
title: 'Downloads',
content: <Downloads />
},
{
title: 'Compute Jobs',
content: (
<OceanProvider>
<ComputeJobs />
</OceanProvider>
)
}
]
export default function HistoryPage(): ReactElement {
const { chainIds } = useUserPreferences()
const url = new URL(window.location.href)
const defaultTab = url.searchParams.get('defaultTab')
let defaultTabIndex = 0
defaultTab === 'ComputeJobs' ? (defaultTabIndex = 4) : (defaultTabIndex = 0)
return (
<article className={styles.content}>
<Tabs
items={tabs}
className={styles.tabs}
defaultIndex={defaultTabIndex}
/>
</article>
)
}

View File

@ -0,0 +1,66 @@
.account {
text-align: center;
}
.account p {
margin: 0;
}
@media (min-width: 40rem) {
.account {
text-align: left;
display: grid;
grid-template-columns: 0.2fr 1.8fr;
gap: calc(var(--spacer) / 2);
align-items: center;
}
}
.imageWrap,
.image {
width: 96px;
height: 96px;
}
.imageWrap {
border-radius: 50%;
overflow: hidden;
margin-bottom: calc(var(--spacer) / 4);
border: 1px solid var(--border-color);
box-shadow: 0 6px 17px 0 var(--box-shadow-color);
}
.image {
max-width: unset;
}
.name {
font-size: var(--font-size-h3);
margin-bottom: calc(var(--spacer) / 8);
}
.accountId {
display: block;
font-size: var(--font-size-small);
color: var(--color-secondary);
word-wrap: break-word;
white-space: pre-wrap;
padding: 0;
margin: 0;
}
.explorer {
font-size: var(--font-size-mini);
margin-right: calc(var(--spacer) / 1.5);
display: inline-flex;
align-items: center;
}
.explorer svg:first-child {
width: var(--font-size-mini);
height: var(--font-size-mini);
}
.explorer svg:last-child {
margin-left: calc(var(--spacer) / 12);
}

View File

@ -0,0 +1,67 @@
import React, { ReactElement } from 'react'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import ExplorerLink from '../../../atoms/ExplorerLink'
import NetworkName from '../../../atoms/NetworkName'
import jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg'
import Copy from '../../../atoms/Copy'
import Blockies from '../../../atoms/Blockies'
import styles from './Account.module.css'
import { useProfile } from '../../../../providers/Profile'
export default function Account({
accountId
}: {
accountId: string
}): ReactElement {
const { chainIds } = useUserPreferences()
const { profile } = useProfile()
return (
<div className={styles.account}>
<figure className={styles.imageWrap}>
{profile?.image ? (
<img
src={profile?.image}
className={styles.image}
width="96"
height="96"
/>
) : accountId ? (
<Blockies accountId={accountId} className={styles.image} />
) : (
<img
src={jellyfish}
className={styles.image}
width="96"
height="96"
/>
)}
</figure>
<div>
<h3 className={styles.name}>{profile?.name}</h3>
{accountId && (
<code
className={styles.accountId}
title={profile?.accountEns ? accountId : null}
>
{profile?.accountEns || accountId} <Copy text={accountId} />
</code>
)}
<p>
{accountId &&
chainIds.map((value) => (
<ExplorerLink
className={styles.explorer}
networkId={value}
path={`address/${accountId}`}
key={value}
>
<NetworkName networkId={value} />
</ExplorerLink>
))}
</p>
</div>
</div>
)
}

View File

@ -1,3 +1,7 @@
.links {
width: 100%;
}
.links,
.links a {
font-size: var(--font-size-small);
@ -7,6 +11,7 @@
.links a {
margin-left: calc(var(--spacer) / 3);
color: inherit;
display: inline-block;
}
.links a:first-child {
@ -19,5 +24,5 @@
}
.linksExternal {
composes: linksExternal from './index.module.css';
composes: linksExternal from '../../../atoms/Publisher/index.module.css';
}

View File

@ -1,17 +1,27 @@
import React, { ReactElement } from 'react'
import classNames from 'classnames/bind'
import { ReactComponent as External } from '../../../../images/external.svg'
import styles from './PublisherLinks.module.css'
import { ProfileLink } from '../../../models/Profile'
import { ReactComponent as External } from '../../../images/external.svg'
import { useProfile } from '../../../../providers/Profile'
const cx = classNames.bind(styles)
export default function PublisherLinks({
links
className
}: {
links: ProfileLink[]
className: string
}): ReactElement {
const { profile } = useProfile()
const styleClasses = cx({
links: true,
[className]: className
})
return (
<div className={styles.links}>
<div className={styleClasses}>
{' — '}
{links?.map((link: ProfileLink) => {
{profile?.links?.map((link) => {
const href =
link.name === 'Twitter'
? `https://twitter.com/${link.value}`

View File

@ -0,0 +1,6 @@
.stats {
display: grid;
gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
margin-top: var(--spacer);
}

View File

@ -0,0 +1,124 @@
import { Logger } from '@oceanprotocol/lib'
import React, { useEffect, useState } from 'react'
import { ReactElement } from 'react-markdown'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import {
getAccountLiquidityInOwnAssets,
getAccountNumberOfOrders,
getAssetsBestPrices,
UserLiquidity,
calculateUserLiquidity
} from '../../../../utils/subgraph'
import Conversion from '../../../atoms/Price/Conversion'
import NumberUnit from '../../../molecules/NumberUnit'
import styles from './Stats.module.css'
import { useProfile } from '../../../../providers/Profile'
import { PoolShares_poolShares as PoolShare } from '../../../../@types/apollo/PoolShares'
async function getPoolSharesLiquidity(
poolShares: PoolShare[]
): Promise<number> {
let totalLiquidity = 0
for (const poolShare of poolShares) {
const poolLiquidity = calculateUserLiquidity(poolShare)
totalLiquidity += poolLiquidity
}
return totalLiquidity
}
export default function Stats({
accountId
}: {
accountId: string
}): ReactElement {
const { chainIds } = useUserPreferences()
const { poolShares, assets, assetsTotal, downloadsTotal } = useProfile()
const [sold, setSold] = useState(0)
const [publisherLiquidity, setPublisherLiquidity] = useState<UserLiquidity>()
const [totalLiquidity, setTotalLiquidity] = useState(0)
useEffect(() => {
if (!accountId) {
setSold(0)
setPublisherLiquidity({ price: '0', oceanBalance: '0' })
setTotalLiquidity(0)
return
}
async function getSales() {
if (!assets) return
try {
const nrOrders = await getAccountNumberOfOrders(assets, chainIds)
setSold(nrOrders)
} catch (error) {
Logger.error(error.message)
}
}
getSales()
}, [accountId, assets])
useEffect(() => {
if (!assets || !accountId || !chainIds) return
async function getPublisherLiquidity() {
try {
const accountPoolAdresses: string[] = []
const assetsPrices = await getAssetsBestPrices(assets)
for (const priceInfo of assetsPrices) {
if (priceInfo.price.type === 'pool') {
accountPoolAdresses.push(priceInfo.price.address.toLowerCase())
}
}
const userLiquidity = await getAccountLiquidityInOwnAssets(
accountId,
chainIds,
accountPoolAdresses
)
setPublisherLiquidity(userLiquidity)
} catch (error) {
Logger.error(error.message)
}
}
getPublisherLiquidity()
}, [assets, accountId])
useEffect(() => {
if (!poolShares) return
async function getTotalLiquidity() {
try {
const totalLiquidity = await getPoolSharesLiquidity(poolShares)
setTotalLiquidity(totalLiquidity)
} catch (error) {
console.error('Error fetching pool shares: ', error.message)
}
}
getTotalLiquidity()
}, [poolShares])
return (
<div className={styles.stats}>
<NumberUnit
label="Liquidity in Own Assets"
value={
<Conversion price={publisherLiquidity?.price} hideApproximateSymbol />
}
/>
<NumberUnit
label="Total Liquidity"
value={<Conversion price={`${totalLiquidity}`} hideApproximateSymbol />}
/>
<NumberUnit label={`Sale${sold === 1 ? '' : 's'}`} value={sold} />
<NumberUnit label="Published" value={assetsTotal} />
<NumberUnit
label={`Download${downloadsTotal === 1 ? '' : 's'}`}
tooltip="Datatoken orders for assets with `access` service, as opposed to `compute`. As one order could allow multiple or infinite downloads this number does not reflect the actual download count of an asset file."
value={downloadsTotal}
/>
</div>
)
}

View File

@ -0,0 +1,54 @@
.grid {
composes: box from '../../../atoms/Box.module.css';
background: var(--background-body-transparent);
backdrop-filter: blur(3px);
position: relative;
}
.description {
color: var(--color-secondary);
font-size: var(--font-size-small);
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
margin-top: var(--spacer);
}
.description p:last-child {
margin-bottom: 0;
}
@media (min-width: 50rem) {
.grid {
display: grid;
gap: var(--spacer);
/* lazy golden ratio */
grid-template-columns: 1.618fr 1fr;
}
.description {
margin-top: calc(var(--spacer) / 2);
-webkit-line-clamp: 7 !important;
}
}
.publisherLinks {
margin-top: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 2);
}
.more {
font-size: var(--font-size-mini);
margin-left: calc(var(--spacer) / 8);
cursor: pointer;
}
.meta {
color: var(--color-secondary);
font-size: var(--font-size-mini);
position: absolute;
right: calc(var(--spacer) / 3);
bottom: calc(var(--spacer) / 6);
}

View File

@ -0,0 +1,75 @@
import React, { ReactElement, useState } from 'react'
import PublisherLinks from './PublisherLinks'
import Markdown from '../../../atoms/Markdown'
import Stats from './Stats'
import Account from './Account'
import styles from './index.module.css'
import { useProfile } from '../../../../providers/Profile'
const isDescriptionTextClamped = () => {
const el = document.getElementById('description')
if (el) return el.scrollHeight > el.clientHeight
}
const LinkExternal = ({ url, text }: { url: string; text: string }) => {
return (
<a href={url} target="_blank" rel="noreferrer">
{text}
</a>
)
}
export default function AccountHeader({
accountId
}: {
accountId: string
}): ReactElement {
const { profile } = useProfile()
const [isShowMore, setIsShowMore] = useState(false)
const toogleShowMore = () => {
setIsShowMore(!isShowMore)
}
return (
<div className={styles.grid}>
<div>
<Account accountId={accountId} />
<Stats accountId={accountId} />
</div>
<div>
<Markdown text={profile?.description} className={styles.description} />
{isDescriptionTextClamped() ? (
<span className={styles.more} onClick={toogleShowMore}>
<LinkExternal
url={`https://www.3box.io/${accountId}`}
text="Read more on 3box"
/>
</span>
) : (
''
)}
{profile?.links?.length > 0 && (
<PublisherLinks className={styles.publisherLinks} />
)}
</div>
<div className={styles.meta}>
Profile data from{' '}
{profile?.accountEns && (
<>
<LinkExternal
url={`https://app.ens.domains/name/${profile.accountEns}`}
text="ENS"
/>{' '}
&{' '}
</>
)}
<LinkExternal
url={`https://www.3box.io/${accountId}`}
text="3Box Hub"
/>
</div>
</div>
)
}

View File

@ -7,7 +7,7 @@
}
.asset {
composes: box from '../../../atoms/Box.module.css';
composes: box from '../../../../atoms/Box.module.css';
box-shadow: none;
padding: calc(var(--spacer) / 2);
margin-bottom: calc(var(--spacer) / 2);

View File

@ -1,16 +1,15 @@
import React, { ReactElement, useEffect, useState } from 'react'
import axios from 'axios'
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
import Time from '../../../atoms/Time'
import Button from '../../../atoms/Button'
import Modal from '../../../atoms/Modal'
import MetaItem from '../../../organisms/AssetContent/MetaItem'
import { ReactComponent as External } from '../../../../images/external.svg'
import { retrieveDDO } from '../../../../utils/aquarius'
import { useOcean } from '../../../../providers/Ocean'
import { ComputeJobMetaData } from '../../../../../@types/ComputeJobMetaData'
import Time from '../../../../atoms/Time'
import Button from '../../../../atoms/Button'
import Modal from '../../../../atoms/Modal'
import MetaItem from '../../../../organisms/AssetContent/MetaItem'
import { ReactComponent as External } from '../../../../../images/external.svg'
import { retrieveDDO } from '../../../../../utils/aquarius'
import Results from './Results'
import styles from './Details.module.css'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import { useSiteMetadata } from '../../../../../hooks/useSiteMetadata'
function Asset({
title,

View File

@ -1,12 +1,12 @@
import { Logger } from '@oceanprotocol/lib'
import React, { ReactElement, useState } from 'react'
import Loader from '../../../atoms/Loader'
import { ComputeJobMetaData } from '../../../../@types/ComputeJobMetaData'
import { ListItem } from '../../../atoms/Lists'
import Button from '../../../atoms/Button'
import { useOcean } from '../../../../providers/Ocean'
import Loader from '../../../../atoms/Loader'
import { ComputeJobMetaData } from '../../../../../@types/ComputeJobMetaData'
import { ListItem } from '../../../../atoms/Lists'
import Button from '../../../../atoms/Button'
import { useOcean } from '../../../../../providers/Ocean'
import styles from './Results.module.css'
import FormHelp from '../../../atoms/Input/Help'
import FormHelp from '../../../../atoms/Input/Help'
import { graphql, useStaticQuery } from 'gatsby'
export const contentQuery = graphql`

View File

@ -0,0 +1,142 @@
import React, { ReactElement, useEffect, useState, useCallback } from 'react'
import Time from '../../../../atoms/Time'
import { Link } from 'gatsby'
import { Logger } from '@oceanprotocol/lib'
import { ComputeJobMetaData } from '../../../../../@types/ComputeJobMetaData'
import Dotdotdot from 'react-dotdotdot'
import Table from '../../../../atoms/Table'
import Button from '../../../../atoms/Button'
import { useOcean } from '../../../../../providers/Ocean'
import { useWeb3 } from '../../../../../providers/Web3'
import Details from './Details'
import { ReactComponent as Refresh } from '../../../../../images/refresh.svg'
import { useUserPreferences } from '../../../../../providers/UserPreferences'
import { getOceanConfig } from '../../../../../utils/ocean'
import NetworkName from '../../../../atoms/NetworkName'
import { getComputeJobs } from '../../../../../utils/compute'
import styles from './index.module.css'
import { useAsset } from '../../../../../providers/Asset'
export function Status({ children }: { children: string }): ReactElement {
return <div className={styles.status}>{children}</div>
}
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: ComputeJobMetaData) {
return (
<Dotdotdot clamp={2}>
<Link to={`/asset/${row.inputDID[0]}`}>{row.assetName}</Link>
</Dotdotdot>
)
}
},
{
name: 'Network',
selector: function getNetwork(row: ComputeJobMetaData) {
return <NetworkName networkId={row.networkId} />
}
},
{
name: 'Created',
selector: function getTimeRow(row: ComputeJobMetaData) {
return <Time date={row.dateCreated} isUnix relative />
}
},
{
name: 'Finished',
selector: function getTimeRow(row: ComputeJobMetaData) {
return row.dateFinished ? (
<Time date={row.dateFinished} isUnix relative />
) : (
''
)
}
},
{
name: 'Status',
selector: function getStatus(row: ComputeJobMetaData) {
return <Status>{row.statusText}</Status>
}
},
{
name: 'Actions',
selector: function getActions(row: ComputeJobMetaData) {
return <Details job={row} />
}
}
]
export default function ComputeJobs({
minimal
}: {
minimal?: boolean
}): ReactElement {
const { ocean, account, config, connect } = useOcean()
const { accountId, networkId } = useWeb3()
const { ddo } = useAsset()
const [isLoading, setIsLoading] = useState(true)
const { chainIds } = useUserPreferences()
const [jobs, setJobs] = useState<ComputeJobMetaData[]>([])
const columnsMinimal = [columns[4], columns[5], columns[3]]
useEffect(() => {
async function initOcean() {
const oceanInitialConfig = getOceanConfig(networkId)
await connect(oceanInitialConfig)
}
if (ocean === undefined) {
initOcean()
}
}, [networkId, ocean, connect])
const fetchJobs = useCallback(async () => {
if (!chainIds || !accountId) {
setIsLoading(false)
return
}
try {
setIsLoading(true)
const jobs = await getComputeJobs(chainIds, config, ocean, account, ddo)
setJobs(jobs)
} catch (error) {
Logger.error(error.message)
} finally {
setIsLoading(false)
}
}, [account, accountId, chainIds, ddo, config, ocean])
useEffect(() => {
fetchJobs()
}, [fetchJobs])
return accountId ? (
<>
{jobs?.length >= 0 && !minimal && (
<Button
style="text"
size="small"
title="Refresh compute jobs"
onClick={async () => await fetchJobs()}
disabled={isLoading}
className={styles.refresh}
>
<Refresh />
Refresh
</Button>
)}
<Table
columns={minimal ? columnsMinimal : columns}
data={jobs}
isLoading={isLoading}
defaultSortField="row.dateCreated"
defaultSortAsc={false}
/>
</>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -0,0 +1,53 @@
import React, { ReactElement } from 'react'
import Table from '../../../atoms/Table'
import Time from '../../../atoms/Time'
import AssetTitle from '../../../molecules/AssetListTitle'
import NetworkName from '../../../atoms/NetworkName'
import { useProfile } from '../../../../providers/Profile'
import { DownloadedAsset } from '../../../../utils/aquarius'
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: DownloadedAsset) {
return <AssetTitle ddo={row.ddo} />
}
},
{
name: 'Network',
selector: function getNetwork(row: DownloadedAsset) {
return <NetworkName networkId={row.networkId} />
}
},
{
name: 'Datatoken',
selector: function getTitleRow(row: DownloadedAsset) {
return row.dtSymbol
}
},
{
name: 'Time',
selector: function getTimeRow(row: DownloadedAsset) {
return <Time date={row.timestamp.toString()} relative isUnix />
}
}
]
export default function ComputeDownloads({
accountId
}: {
accountId: string
}): ReactElement {
const { downloads, isDownloadsLoading } = useProfile()
return accountId ? (
<Table
columns={columns}
data={downloads}
paginationPerPage={10}
isLoading={isDownloadsLoading}
/>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -30,31 +30,31 @@
}
}
.yourLiquidity {
.userLiquidity {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.yourLiquidity [class*='Conversion-module--'] {
.userLiquidity [class*='Conversion-module--'] {
margin-bottom: calc(var(--spacer) / 8);
}
.yourLiquidity [class*='Conversion-module--'] strong {
.userLiquidity [class*='Conversion-module--'] strong {
font-size: var(--font-size-base);
}
.yourLiquidity [class*='Token-module--token'] {
.userLiquidity [class*='Token-module--token'] {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: calc(var(--spacer) / 8);
}
.yourLiquidity [class*='Token-module--token'] div {
.userLiquidity [class*='Token-module--token'] div {
font-size: var(--font-size-small);
}
.yourLiquidity [class*='Token-module--icon'] {
.userLiquidity [class*='Token-module--icon'] {
display: none;
}

View File

@ -0,0 +1,217 @@
import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import Table from '../../../atoms/Table'
import Conversion from '../../../atoms/Price/Conversion'
import styles from './PoolShares.module.css'
import AssetTitle from '../../../molecules/AssetListTitle'
import {
PoolShares_poolShares as PoolShare,
PoolShares_poolShares_poolId_tokens as PoolSharePoolIdTokens
} from '../../../../@types/apollo/PoolShares'
import web3 from 'web3'
import Token from '../../../organisms/AssetActions/Pool/Token'
import { calculateUserLiquidity } from '../../../../utils/subgraph'
import NetworkName from '../../../atoms/NetworkName'
import axios, { CancelToken } from 'axios'
import { retrieveDDO } from '../../../../utils/aquarius'
import { isValidNumber } from '../../../../utils/numberValidations'
import Decimal from 'decimal.js'
import { useProfile } from '../../../../providers/Profile'
import { DDO } from '@oceanprotocol/lib'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
const REFETCH_INTERVAL = 20000
interface Asset {
userLiquidity: number
poolShare: PoolShare
networkId: number
createTime: number
ddo: DDO
}
function findTokenByType(tokens: PoolSharePoolIdTokens[], type: string) {
const { symbol } = tokens.find((token) =>
type === 'datatoken'
? token.isDatatoken === true
: token.isDatatoken === false
)
return symbol
}
function Symbol({ tokens }: { tokens: PoolSharePoolIdTokens[] }) {
return <>{findTokenByType(tokens, 'datatoken')}</>
}
function Liquidity({ row, type }: { row: Asset; type: string }) {
let price = ``
let oceanTokenBalance = ''
let dataTokenBalance = ''
if (type === 'user') {
price = `${row.userLiquidity}`
const userShare = row.poolShare.balance / row.poolShare.poolId.totalShares
oceanTokenBalance = (
userShare * row.poolShare.poolId.oceanReserve
).toString()
dataTokenBalance = (
userShare * row.poolShare.poolId.datatokenReserve
).toString()
}
if (type === 'pool') {
price =
isValidNumber(row.poolShare.poolId.oceanReserve) &&
isValidNumber(row.poolShare.poolId.datatokenReserve) &&
isValidNumber(row.poolShare.poolId.consumePrice)
? new Decimal(row.poolShare.poolId.datatokenReserve)
.mul(new Decimal(row.poolShare.poolId.consumePrice))
.plus(row.poolShare.poolId.oceanReserve)
.toString()
: '0'
oceanTokenBalance = row.poolShare.poolId.oceanReserve.toString()
dataTokenBalance = row.poolShare.poolId.datatokenReserve.toString()
}
return (
<div className={styles.userLiquidity}>
<Conversion
price={price}
className={styles.totalLiquidity}
hideApproximateSymbol
/>
<Token
symbol={findTokenByType(row.poolShare.poolId.tokens, 'ocean')}
balance={oceanTokenBalance}
noIcon
/>
<Token
symbol={findTokenByType(row.poolShare.poolId.tokens, 'datatoken')}
balance={dataTokenBalance}
noIcon
/>
</div>
)
}
const columns = [
{
name: 'Data Set',
selector: function getAssetRow(row: Asset) {
return <AssetTitle ddo={row.ddo} />
},
grow: 2
},
{
name: 'Network',
selector: function getNetwork(row: Asset) {
return <NetworkName networkId={row.networkId} />
}
},
{
name: 'Datatoken',
selector: function getSymbol(row: Asset) {
return <Symbol tokens={row.poolShare.poolId.tokens} />
}
},
{
name: 'Liquidity',
selector: function getAssetRow(row: Asset) {
return <Liquidity row={row} type="user" />
},
right: true
},
{
name: 'Pool Liquidity',
selector: function getAssetRow(row: Asset) {
return <Liquidity row={row} type="pool" />
},
right: true
}
]
async function getPoolSharesAssets(
data: PoolShare[],
cancelToken: CancelToken
) {
const assetList: Asset[] = []
for (let i = 0; i < data.length; i++) {
const did = web3.utils
.toChecksumAddress(data[i].poolId.datatokenAddress)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, cancelToken)
const userLiquidity = calculateUserLiquidity(data[i])
ddo &&
assetList.push({
poolShare: data[i],
userLiquidity: userLiquidity,
networkId: ddo?.chainId,
createTime: data[i].poolId.createTime,
ddo
})
}
const assets = assetList.sort((a, b) => b.createTime - a.createTime)
return assets
}
export default function PoolShares({
accountId
}: {
accountId: string
}): ReactElement {
const { poolShares, isPoolSharesLoading } = useProfile()
const [assets, setAssets] = useState<Asset[]>()
const [loading, setLoading] = useState<boolean>(false)
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
const fetchPoolSharesAssets = useCallback(
async (cancelToken: CancelToken) => {
if (!poolShares || isPoolSharesLoading) return
try {
const assets = await getPoolSharesAssets(poolShares, cancelToken)
setAssets(assets)
} catch (error) {
console.error('Error fetching pool shares: ', error.message)
}
},
[poolShares, isPoolSharesLoading]
)
useEffect(() => {
const cancelTokenSource = axios.CancelToken.source()
async function init() {
setLoading(true)
await fetchPoolSharesAssets(cancelTokenSource.token)
setLoading(false)
if (dataFetchInterval) return
const interval = setInterval(async () => {
await fetchPoolSharesAssets(cancelTokenSource.token)
}, REFETCH_INTERVAL)
setDataFetchInterval(interval)
}
init()
return () => {
clearInterval(dataFetchInterval)
cancelTokenSource.cancel()
}
}, [dataFetchInterval, fetchPoolSharesAssets])
return accountId ? (
<Table
columns={columns}
className={styles.poolSharesTable}
data={assets}
pagination
paginationPerPage={5}
isLoading={loading}
sortField="userLiquidity"
sortAsc={false}
/>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -0,0 +1,7 @@
.filters {
margin-top: -1rem;
}
.assets {
margin-top: calc(var(--spacer) / 3);
}

View File

@ -0,0 +1,77 @@
import { Logger } from '@oceanprotocol/lib'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import React, { ReactElement, useEffect, useState } from 'react'
import AssetList from '../../../organisms/AssetList'
import { getPublishedAssets } from '../../../../utils/aquarius'
// import Filters from '../../../templates/Search/Filters'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../../providers/UserPreferences'
import styles from './PublishedList.module.css'
import axios from 'axios'
export default function PublishedList({
accountId
}: {
accountId: string
}): ReactElement {
const { appConfig } = useSiteMetadata()
const { chainIds } = useUserPreferences()
const [queryResult, setQueryResult] = useState<QueryResult>()
const [isLoading, setIsLoading] = useState(false)
const [page, setPage] = useState<number>(1)
const [service, setServiceType] = useState('dataset OR algorithm')
useEffect(() => {
if (!accountId) return
const cancelTokenSource = axios.CancelToken.source()
async function getPublished() {
try {
setIsLoading(true)
const result = await getPublishedAssets(
accountId,
chainIds,
cancelTokenSource.token,
page,
service
)
setQueryResult(result)
} catch (error) {
Logger.error(error.message)
} finally {
setIsLoading(false)
}
}
getPublished()
return () => {
cancelTokenSource.cancel()
}
}, [accountId, page, appConfig.metadataCacheUri, chainIds, service])
return accountId ? (
<>
{/* <Filters
serviceType={service}
setServiceType={setServiceType}
className={styles.filters}
/> */}
<AssetList
assets={queryResult?.results}
isLoading={isLoading}
showPagination
page={queryResult?.page}
totalPages={queryResult?.totalPages}
onPageChange={(newPage) => {
setPage(newPage)
}}
className={styles.assets}
noPublisher
/>
</>
) : (
<div>Please connect your Web3 wallet.</div>
)
}

View File

@ -16,7 +16,7 @@
}
}
.content {
.tabs {
margin-top: var(--spacer);
background-color: var(--background-body);
}

View File

@ -0,0 +1,69 @@
import React, { ReactElement } from 'react'
import Tabs from '../../../atoms/Tabs'
import PoolShares from './PoolShares'
import PoolTransactions from '../../../molecules/PoolTransactions'
import PublishedList from './PublishedList'
import Downloads from './Downloads'
import ComputeJobs from './ComputeJobs'
import { useLocation } from '@reach/router'
import styles from './index.module.css'
import OceanProvider from '../../../../providers/Ocean'
import { useWeb3 } from '../../../../providers/Web3'
interface HistoryTab {
title: string
content: JSX.Element
}
function getTabs(accountId: string, userAccountId: string): HistoryTab[] {
const defaultTabs: HistoryTab[] = [
{
title: 'Published',
content: <PublishedList accountId={accountId} />
},
{
title: 'Pool Shares',
content: <PoolShares accountId={accountId} />
},
{
title: 'Pool Transactions',
content: <PoolTransactions accountId={accountId} />
},
{
title: 'Downloads',
content: <Downloads accountId={accountId} />
}
]
const computeTab: HistoryTab = {
title: 'Compute Jobs',
content: (
<OceanProvider>
<ComputeJobs />
</OceanProvider>
)
}
if (accountId === userAccountId) {
defaultTabs.push(computeTab)
}
return defaultTabs
}
export default function HistoryPage({
accountIdentifier
}: {
accountIdentifier: string
}): ReactElement {
const { accountId } = useWeb3()
const location = useLocation()
const url = new URL(location.href)
const defaultTab = url.searchParams.get('defaultTab')
const tabs = getTabs(accountIdentifier, accountId)
let defaultTabIndex = 0
defaultTab === 'ComputeJobs' ? (defaultTabIndex = 4) : (defaultTabIndex = 0)
return (
<Tabs items={tabs} className={styles.tabs} defaultIndex={defaultTabIndex} />
)
}

View File

@ -0,0 +1,16 @@
import React, { ReactElement } from 'react'
import HistoryPage from './History'
import AccountHeader from './Header'
export default function AccountPage({
accountId
}: {
accountId: string
}): ReactElement {
return (
<>
<AccountHeader accountId={accountId} />
<HistoryPage accountIdentifier={accountId} />
</>
)
}

View File

@ -5,7 +5,7 @@ import Container from '../atoms/Container'
export interface PageProps {
children: ReactNode
title: string
title?: string
uri: string
description?: string
noPageHeader?: boolean

View File

@ -50,10 +50,6 @@ button.filter,
color: var(--background-body);
}
.filterList:first-of-type {
margin-bottom: calc(var(--spacer) / 6);
}
.showClear:hover {
display: inline-flex;
color: var(--color-primary);

View File

@ -1,6 +1,5 @@
import React, { ReactElement, useState } from 'react'
import { useNavigate } from '@reach/router'
import styles from './filterService.module.css'
import classNames from 'classnames/bind'
import {
addExistingParamsToUrl,
@ -8,6 +7,7 @@ import {
FilterByTypeOptions
} from './utils'
import Button from '../../atoms/Button'
import styles from './Filters.module.css'
const cx = classNames.bind(styles)

View File

@ -2,15 +2,15 @@ import React, { ReactElement, useState, useEffect } from 'react'
import Permission from '../../organisms/Permission'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import AssetList from '../../organisms/AssetList'
import styles from './index.module.css'
import queryString from 'query-string'
import ServiceFilter from './filterService'
import Filters from './Filters'
import Sort from './sort'
import { getResults } from './utils'
import { navigate } from 'gatsby'
import { updateQueryStringParameter } from '../../../utils'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../providers/UserPreferences'
import styles from './index.module.css'
export default function SearchPage({
location,
@ -78,7 +78,7 @@ export default function SearchPage({
<>
<div className={styles.search}>
<div className={styles.row}>
<ServiceFilter
<Filters
serviceType={service}
accessType={access}
setServiceType={setServiceType}

View File

@ -4,8 +4,9 @@ export interface ProfileLink {
}
export interface Profile {
did: string
did?: string
name?: string
accountEns?: string
description?: string
emoji?: string
image?: string

View File

@ -1,30 +0,0 @@
import React, { ReactElement } from 'react'
import PageHistory from '../components/pages/History'
import Page from '../components/templates/Page'
import { graphql, PageProps } from 'gatsby'
export default function PageGatsbyHistory(props: PageProps): ReactElement {
const content = (props.data as any).content.edges[0].node.childPagesJson
const { title, description } = content
return (
<Page title={title} description={description} uri={props.uri}>
<PageHistory />
</Page>
)
}
export const contentQuery = graphql`
query HistoryPageQuery {
content: allFile(filter: { relativePath: { eq: "pages/history.json" } }) {
edges {
node {
childPagesJson {
title
description
}
}
}
}
}
`

View File

@ -0,0 +1,80 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Page from '../../components/templates/Page'
import { graphql, PageProps, navigate } from 'gatsby'
import ProfilePage from '../../components/pages/Profile'
import { accountTruncate } from '../../utils/web3'
import { useWeb3 } from '../../providers/Web3'
import ProfileProvider from '../../providers/Profile'
import { getEnsAddress, getEnsName } from '../../utils/ens'
import ethereumAddress from 'ethereum-address'
export default function PageGatsbyProfile(props: PageProps): ReactElement {
const { accountId, accountEns } = useWeb3()
const [finalAccountId, setFinalAccountId] = useState<string>()
const [finalAccountEns, setFinalAccountEns] = useState<string>()
// Have accountId in path take over, if not present fall back to web3
useEffect(() => {
async function init() {
if (!props?.location?.pathname) return
// Path is root /profile, have web3 take over
if (props.location.pathname === '/profile') {
setFinalAccountEns(accountEns)
setFinalAccountId(accountId)
return
}
const pathAccount = props.location.pathname.split('/')[2]
// Path has ETH addreess
if (ethereumAddress.isAddress(pathAccount)) {
const finalAccountId = pathAccount || accountId
setFinalAccountId(finalAccountId)
const accountEns = await getEnsName(finalAccountId)
if (!accountEns) return
setFinalAccountEns(accountEns)
} else {
// Path has ENS name
setFinalAccountEns(pathAccount)
const resolvedAccountId = await getEnsAddress(pathAccount)
setFinalAccountId(resolvedAccountId)
}
}
init()
}, [props.location.pathname, accountId, accountEns])
// Replace pathname with ENS name if present
useEffect(() => {
if (!finalAccountEns || props.location.pathname === '/profile') return
const newProfilePath = `/profile/${finalAccountEns}`
// make sure we only replace path once
if (newProfilePath !== props.location.pathname)
navigate(newProfilePath, { replace: true })
}, [props.location, finalAccountEns, accountId])
return (
<Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader>
<ProfileProvider accountId={finalAccountId} accountEns={finalAccountEns}>
<ProfilePage accountId={finalAccountId} />
</ProfileProvider>
</Page>
)
}
export const contentQuery = graphql`
query ProfilePageQuery {
content: allFile(filter: { relativePath: { eq: "pages/profile.json" } }) {
edges {
node {
childPagesJson {
title
description
}
}
}
}
}
`

299
src/providers/Profile.tsx Normal file
View File

@ -0,0 +1,299 @@
import React, {
useContext,
useState,
useEffect,
createContext,
ReactElement,
useCallback,
ReactNode
} from 'react'
import { getPoolSharesData, getUserTokenOrders } from '../utils/subgraph'
import { useUserPreferences } from './UserPreferences'
import { PoolShares_poolShares as PoolShare } from '../@types/apollo/PoolShares'
import { DDO, Logger } from '@oceanprotocol/lib'
import {
DownloadedAsset,
getDownloadAssets,
getPublishedAssets
} from '../utils/aquarius'
import { useSiteMetadata } from '../hooks/useSiteMetadata'
import { Profile } from '../models/Profile'
import { accountTruncate } from '../utils/web3'
import axios, { CancelToken } from 'axios'
import ethereumAddress from 'ethereum-address'
import get3BoxProfile from '../utils/profile'
import web3 from 'web3'
interface ProfileProviderValue {
profile: Profile
poolShares: PoolShare[]
isPoolSharesLoading: boolean
assets: DDO[]
assetsTotal: number
isEthAddress: boolean
downloads: DownloadedAsset[]
downloadsTotal: number
isDownloadsLoading: boolean
}
const ProfileContext = createContext({} as ProfileProviderValue)
const refreshInterval = 10000 // 10 sec.
function ProfileProvider({
accountId,
accountEns,
children
}: {
accountId: string
accountEns: string
children: ReactNode
}): ReactElement {
const { chainIds } = useUserPreferences()
const { appConfig } = useSiteMetadata()
const [isEthAddress, setIsEthAddress] = useState<boolean>()
//
// Do nothing in all following effects
// when accountId is no ETH address
//
useEffect(() => {
const isEthAddress = ethereumAddress.isAddress(accountId)
setIsEthAddress(isEthAddress)
}, [accountId])
//
// User profile: ENS + 3Box
//
const [profile, setProfile] = useState<Profile>()
useEffect(() => {
if (!accountEns) return
Logger.log(`[profile] ENS name found for ${accountId}:`, accountEns)
}, [accountId, accountEns])
useEffect(() => {
const clearedProfile: Profile = {
name: null,
accountEns: null,
image: null,
description: null,
links: null
}
if (!accountId || !isEthAddress) {
setProfile(clearedProfile)
return
}
const cancelTokenSource = axios.CancelToken.source()
async function getInfo() {
setProfile({ name: accountEns || accountTruncate(accountId), accountEns })
const profile3Box = await get3BoxProfile(
accountId,
cancelTokenSource.token
)
if (profile3Box) {
const { name, emoji, description, image, links } = profile3Box
const newName = `${emoji || ''} ${name || accountTruncate(accountId)}`
const newProfile = {
name: newName,
image,
description,
links
}
setProfile((prevState) => ({
...prevState,
...newProfile
}))
Logger.log('[profile] Found and set 3box profile.', newProfile)
} else {
// setProfile(clearedProfile)
Logger.log('[profile] No 3box profile found.')
}
}
getInfo()
return () => {
cancelTokenSource.cancel()
}
}, [accountId, accountEns, isEthAddress])
//
// POOL SHARES
//
const [poolShares, setPoolShares] = useState<PoolShare[]>()
const [isPoolSharesLoading, setIsPoolSharesLoading] = useState<boolean>(false)
const [poolSharesInterval, setPoolSharesInterval] = useState<NodeJS.Timeout>()
const fetchPoolShares = useCallback(async () => {
if (!accountId || !chainIds || !isEthAddress) return
try {
setIsPoolSharesLoading(true)
const poolShares = await getPoolSharesData(accountId, chainIds)
setPoolShares(poolShares)
Logger.log(
`[profile] Fetched ${poolShares.length} pool shares.`,
poolShares
)
} catch (error) {
Logger.error('Error fetching pool shares: ', error.message)
} finally {
setIsPoolSharesLoading(false)
}
}, [accountId, chainIds, isEthAddress])
useEffect(() => {
async function init() {
await fetchPoolShares()
if (poolSharesInterval) return
const interval = setInterval(async () => {
await fetchPoolShares()
}, refreshInterval)
setPoolSharesInterval(interval)
}
init()
return () => {
clearInterval(poolSharesInterval)
}
}, [poolSharesInterval, fetchPoolShares])
//
// PUBLISHED ASSETS
//
const [assets, setAssets] = useState<DDO[]>()
const [assetsTotal, setAssetsTotal] = useState(0)
// const [assetsWithPrices, setAssetsWithPrices] = useState<AssetListPrices[]>()
useEffect(() => {
if (!accountId || !isEthAddress) return
const cancelTokenSource = axios.CancelToken.source()
async function getAllPublished() {
try {
const result = await getPublishedAssets(
accountId,
chainIds,
cancelTokenSource.token
)
setAssets(result.results)
setAssetsTotal(result.totalResults)
Logger.log(
`[profile] Fetched ${result.totalResults} assets.`,
result.results
)
// Hint: this would only make sense if we "search" in all subcomponents
// against this provider's state, meaning filtering via js rather then sending
// more queries to Aquarius.
// const assetsWithPrices = await getAssetsBestPrices(result.results)
// setAssetsWithPrices(assetsWithPrices)
} catch (error) {
Logger.error(error.message)
}
}
getAllPublished()
return () => {
cancelTokenSource.cancel()
}
}, [accountId, appConfig.metadataCacheUri, chainIds, isEthAddress])
//
// DOWNLOADS
//
const [downloads, setDownloads] = useState<DownloadedAsset[]>()
const [downloadsTotal, setDownloadsTotal] = useState(0)
const [isDownloadsLoading, setIsDownloadsLoading] = useState<boolean>()
const [downloadsInterval, setDownloadsInterval] = useState<NodeJS.Timeout>()
const fetchDownloads = useCallback(
async (cancelToken: CancelToken) => {
if (!accountId || !chainIds) return
const didList: string[] = []
const tokenOrders = await getUserTokenOrders(accountId, chainIds)
for (let i = 0; i < tokenOrders?.length; i++) {
const did = web3.utils
.toChecksumAddress(tokenOrders[i].datatokenId.address)
.replace('0x', 'did:op:')
didList.push(did)
}
const downloads = await getDownloadAssets(
didList,
tokenOrders,
chainIds,
cancelToken
)
setDownloads(downloads)
setDownloadsTotal(downloads.length)
Logger.log(
`[profile] Fetched ${downloads.length} download orders.`,
downloads
)
},
[accountId, chainIds]
)
useEffect(() => {
const cancelTokenSource = axios.CancelToken.source()
async function getDownloadAssets() {
if (!appConfig?.metadataCacheUri) return
try {
setIsDownloadsLoading(true)
await fetchDownloads(cancelTokenSource.token)
} catch (err) {
Logger.log(err.message)
} finally {
setIsDownloadsLoading(false)
}
}
getDownloadAssets()
if (downloadsInterval) return
const interval = setInterval(async () => {
await fetchDownloads(cancelTokenSource.token)
}, refreshInterval)
setDownloadsInterval(interval)
return () => {
cancelTokenSource.cancel()
clearInterval(downloadsInterval)
}
}, [fetchDownloads, appConfig.metadataCacheUri, downloadsInterval])
return (
<ProfileContext.Provider
value={{
profile,
poolShares,
isPoolSharesLoading,
assets,
assetsTotal,
isEthAddress,
downloads,
downloadsTotal,
isDownloadsLoading
}}
>
{children}
</ProfileContext.Provider>
)
}
// Helper hook to access the provider values
const useProfile = (): ProfileProviderValue => useContext(ProfileContext)
export { ProfileProvider, useProfile, ProfileProviderValue, ProfileContext }
export default ProfileProvider

View File

@ -18,7 +18,7 @@ import {
getNetworkDataById,
getNetworkDisplayName
} from '../utils/web3'
import { graphql } from 'gatsby'
import { getEnsName } from '../utils/ens'
import { UserBalance } from '../@types/TokenBalance'
import { getOceanBalance } from '../utils/ocean'
import useNetworkMetadata from '../hooks/useNetworkMetadata'
@ -29,6 +29,7 @@ interface Web3ProviderValue {
web3Modal: Web3Modal
web3ProviderInfo: IProviderInfo
accountId: string
accountEns: string
balance: UserBalance
networkId: number
chainId: number
@ -84,26 +85,6 @@ export const web3ModalOpts = {
const refreshInterval = 20000 // 20 sec.
const networksQuery = graphql`
query {
allNetworksMetadataJson {
edges {
node {
chain
network
networkId
chainId
nativeCurrency {
name
symbol
decimals
}
}
}
}
}
`
const Web3Context = createContext({} as Web3ProviderValue)
function Web3Provider({ children }: { children: ReactNode }): ReactElement {
@ -120,6 +101,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
const [block, setBlock] = useState<number>()
const [isTestnet, setIsTestnet] = useState<boolean>()
const [accountId, setAccountId] = useState<string>()
const [accountEns, setAccountEns] = useState<string>()
const [web3Loading, setWeb3Loading] = useState<boolean>(true)
const [balance, setBalance] = useState<UserBalance>({
eth: '0',
@ -181,6 +163,27 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
}
}, [accountId, networkId, web3])
// -----------------------------------
// Helper: Get user ENS name
// -----------------------------------
const getUserEnsName = useCallback(async () => {
if (!accountId) return
try {
// const accountEns = await getEnsNameWithWeb3(
// accountId,
// web3Provider,
// `${networkId}`
// )
const accountEns = await getEnsName(accountId)
setAccountEns(accountEns)
accountEns &&
Logger.log(`[web3] ENS name found for ${accountId}:`, accountEns)
} catch (error) {
Logger.error('[web3] Error: ', error.message)
}
}, [accountId])
// -----------------------------------
// Create initial Web3Modal instance
// -----------------------------------
@ -229,6 +232,13 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
}
}, [getUserBalance])
// -----------------------------------
// Get and set user ENS name
// -----------------------------------
useEffect(() => {
getUserEnsName()
}, [getUserEnsName])
// -----------------------------------
// Get and set network metadata
// -----------------------------------
@ -333,6 +343,7 @@ function Web3Provider({ children }: { children: ReactNode }): ReactElement {
web3Modal,
web3ProviderInfo,
accountId,
accountEns,
balance,
networkId,
chainId,

View File

@ -12,7 +12,16 @@ import {
import { AssetSelectionAsset } from '../components/molecules/FormFields/AssetSelection'
import { PriceList, getAssetsPriceList } from './subgraph'
import axios, { CancelToken, AxiosResponse } from 'axios'
import { OrdersData_tokenOrders as OrdersData } from '../@types/apollo/OrdersData'
import { metadataCacheUri } from '../../app.config'
import web3 from '../../tests/unit/__mocks__/web3'
export interface DownloadedAsset {
dtSymbol: string
timestamp: number
networkId: number
ddo: DDO
}
export const MAXIMUM_NUMBER_OF_PAGES_WITH_RESULTS = 476
@ -68,7 +77,8 @@ export async function queryMetadata(
try {
const response: AxiosResponse<any> = await axios.post(
`${metadataCacheUri}/api/v1/aquarius/assets/ddo/query`,
{ ...query, cancelToken }
{ ...query },
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return
return transformQueryResult(response.data)
@ -110,10 +120,8 @@ export async function getAssetsNames(
try {
const response: AxiosResponse<Record<string, string>> = await axios.post(
`${metadataCacheUri}/api/v1/aquarius/assets/names`,
{
didList,
cancelToken
}
{ didList },
{ cancelToken }
)
if (!response || response.status !== 200 || !response.data) return
return response.data
@ -206,3 +214,122 @@ export async function getAlgorithmDatasetsForCompute(
)
return datasets
}
export async function getPublishedAssets(
accountId: string,
chainIds: number[],
cancelToken: CancelToken,
page?: number,
type?: string
): Promise<QueryResult> {
if (!accountId) return
page = page || 1
type = type || 'dataset OR algorithm'
const queryPublishedAssets = {
page,
offset: 9,
query: {
query_string: {
query: `(publicKey.owner:${accountId}) AND (service.attributes.main.type:${type}) AND (${transformChainIdsListToQuery(
chainIds
)})`
}
},
sort: { created: -1 }
}
try {
const result = await queryMetadata(queryPublishedAssets, cancelToken)
return result
} catch (error) {
if (axios.isCancel(error)) {
Logger.log(error.message)
} else {
Logger.error(error.message)
}
}
}
export async function getAssetsFromDidList(
didList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<QueryResult> {
try {
// TODO: figure out cleaner way to transform string[] into csv
const searchDids = JSON.stringify(didList)
.replace(/,/g, ' ')
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
// for whatever reason ddo.id is not searchable, so use ddo.dataToken instead
.replace(/(did:op:)/g, '0x')
// safeguard against passed empty didList, preventing 500 from Aquarius
if (!searchDids) return
const query = {
page: 1,
offset: 1000,
query: {
query_string: {
query: `(${searchDids}) AND (${transformChainIdsListToQuery(
chainIds
)})`,
fields: ['dataToken'],
default_operator: 'OR'
}
},
sort: { created: -1 }
}
const queryResult = await queryMetadata(query, cancelToken)
return queryResult
} catch (error) {
Logger.error(error.message)
}
}
export async function getDownloadAssets(
didList: string[],
tokenOrders: OrdersData[],
chainIds: number[],
cancelToken: CancelToken
): Promise<DownloadedAsset[]> {
const downloadedAssets: DownloadedAsset[] = []
try {
const queryResult = await getAssetsFromDidList(
didList,
chainIds,
cancelToken
)
const ddoList = queryResult?.results
for (let i = 0; i < tokenOrders?.length; i++) {
const ddo = ddoList.filter(
(ddo) =>
tokenOrders[i].datatokenId.address.toLowerCase() ===
ddo.dataToken.toLowerCase()
)[0]
// make sure we are only pushing download orders
if (ddo.service[1].type !== 'access') continue
downloadedAssets.push({
ddo,
networkId: ddo.chainId,
dtSymbol: tokenOrders[i].datatokenId.symbol,
timestamp: tokenOrders[i].timestamp
})
}
const sortedOrders = downloadedAssets.sort(
(a, b) => b.timestamp - a.timestamp
)
return sortedOrders
} catch (error) {
Logger.error(error.message)
}
}

View File

@ -1,10 +1,272 @@
import {
DDO,
Ocean,
ServiceComputePrivacy,
publisherTrustedAlgorithm as PublisherTrustedAlgorithm
publisherTrustedAlgorithm as PublisherTrustedAlgorithm,
DDO,
Service,
Logger,
Provider,
Config,
Ocean,
Account
} from '@oceanprotocol/lib'
import { ComputePrivacyForm } from '../models/FormEditComputeDataset'
import web3 from 'web3'
import { ComputeJob } from '@oceanprotocol/lib/dist/node/ocean/interfaces/Compute'
import axios, { CancelToken } from 'axios'
import { gql } from 'urql'
import { ComputeJobMetaData } from '../@types/ComputeJobMetaData'
import { transformChainIdsListToQuery, queryMetadata } from './aquarius'
import { fetchDataForMultipleChains } from './subgraph'
import { OrdersData_tokenOrders_datatokenId as OrdersDatatoken } from '../@types/apollo/OrdersData'
const getComputeOrders = gql`
query ComputeOrders($user: String!) {
tokenOrders(
orderBy: timestamp
orderDirection: desc
where: { payer: $user }
) {
id
serviceId
datatokenId {
address
}
tx
timestamp
}
}
`
const getComputeOrdersByDatatokenAddress = gql`
query ComputeOrdersByDatatokenAddress(
$user: String!
$datatokenAddress: String!
) {
tokenOrders(
orderBy: timestamp
orderDirection: desc
where: { payer: $user, datatokenId: $datatokenAddress }
) {
id
serviceId
datatokenId {
address
}
tx
timestamp
}
}
`
interface TokenOrder {
id: string
serviceId: number
datatokenId: OrdersDatatoken
tx: any
timestamp: number
}
async function getAssetMetadata(
queryDtList: string,
cancelToken: CancelToken,
chainIds: number[]
): Promise<DDO[]> {
const queryDid = {
page: 1,
offset: 100,
query: {
query_string: {
query: `(${queryDtList}) AND (${transformChainIdsListToQuery(
chainIds
)}) AND service.attributes.main.type:dataset AND service.type:compute`,
fields: ['dataToken']
}
}
}
const result = await queryMetadata(queryDid, cancelToken)
return result.results
}
function getServiceEndpoints(data: TokenOrder[], assets: DDO[]): string[] {
const serviceEndpoints: string[] = []
for (let i = 0; i < data.length; i++) {
try {
const did = web3.utils
.toChecksumAddress(data[i].datatokenId.address)
.replace('0x', 'did:op:')
const ddo = assets.filter((x) => x.id === did)[0]
if (ddo === undefined) continue
const service = ddo.service.filter(
(x: Service) => x.index === data[i].serviceId
)[0]
if (!service || service.type !== 'compute') continue
const { serviceEndpoint } = service
const wasProviderQueried =
serviceEndpoints?.filter((x) => x === serviceEndpoint).length > 0
if (wasProviderQueried) continue
serviceEndpoints.push(serviceEndpoint)
} catch (err) {
Logger.error(err.message)
}
}
return serviceEndpoints
}
async function getProviders(
serviceEndpoints: string[],
config: Config,
ocean: Ocean
): Promise<Provider[]> {
const providers: Provider[] = []
try {
for (let i = 0; i < serviceEndpoints?.length; i++) {
const instanceConfig = {
config,
web3: config.web3Provider,
logger: Logger,
ocean
}
const provider = await Provider.getInstance(instanceConfig)
await provider.setBaseUrl(serviceEndpoints[i])
const hasSameCompute =
providers.filter((x) => x.computeAddress === provider.computeAddress)
.length > 0
if (!hasSameCompute) providers.push(provider)
}
} catch (err) {
Logger.error(err.message)
}
return providers
}
async function getJobs(
providers: Provider[],
account: Account,
assets: DDO[]
): Promise<ComputeJobMetaData[]> {
const computeJobs: ComputeJobMetaData[] = []
for (let i = 0; i < providers.length; i++) {
try {
const providerComputeJobs = (await providers[i].computeStatus(
'',
account,
undefined,
undefined,
false
)) as ComputeJob[]
// means the provider uri is not good, so we ignore it and move on
if (!providerComputeJobs) continue
providerComputeJobs.sort((a, b) => {
if (a.dateCreated > b.dateCreated) {
return -1
}
if (a.dateCreated < b.dateCreated) {
return 1
}
return 0
})
for (let j = 0; j < providerComputeJobs?.length; j++) {
const job = providerComputeJobs[j]
const did = job.inputDID[0]
const ddo = assets.filter((x) => x.id === did)[0]
if (!ddo) continue
const serviceMetadata = ddo.service.filter(
(x: Service) => x.type === 'metadata'
)[0]
const compJob: ComputeJobMetaData = {
...job,
assetName: serviceMetadata.attributes.main.name,
assetDtSymbol: ddo.dataTokenInfo.symbol,
networkId: ddo.chainId
}
computeJobs.push(compJob)
}
} catch (err) {
Logger.error(err.message)
}
}
return computeJobs
}
function getDtList(data: TokenOrder[]) {
const dtList = []
for (let i = 0; i < data.length; i++) {
dtList.push(data[i].datatokenId.address)
}
const queryDtList = JSON.stringify(dtList)
.replace(/,/g, ' ')
.replace(/"/g, '')
.replace(/(\[|\])/g, '')
return queryDtList
}
export async function getComputeJobs(
chainIds: number[],
config: Config,
ocean: Ocean,
account: Account,
ddo?: DDO
): Promise<ComputeJobMetaData[]> {
const assetDTAddress = ddo?.dataTokenInfo?.address
const variables = assetDTAddress
? {
user: account?.getId().toLowerCase(),
datatokenAddress: assetDTAddress.toLowerCase()
}
: {
user: account?.getId().toLowerCase()
}
const result = await fetchDataForMultipleChains(
assetDTAddress ? getComputeOrdersByDatatokenAddress : getComputeOrders,
variables,
assetDTAddress ? [ddo?.chainId] : chainIds
)
let data: TokenOrder[] = []
for (let i = 0; i < result.length; i++) {
if (!result[i]?.tokenOrders) continue
result[i]?.tokenOrders.forEach((tokenOrder: TokenOrder) => {
data.push(tokenOrder)
})
}
if (!ocean || !account || !data) return
data = data.sort((a, b) => b.timestamp - a.timestamp)
const queryDtList = getDtList(data)
if (queryDtList === '') return
const source = axios.CancelToken.source()
const assets = await getAssetMetadata(queryDtList, source.token, chainIds)
const serviceEndpoints = getServiceEndpoints(data, assets)
const providers: Provider[] = await getProviders(
serviceEndpoints,
config,
ocean
)
const computeJobs = await getJobs(providers, account, assets)
return computeJobs
}
export async function createTrustedAlgorithmList(
selectedAlgorithms: string[], // list of DIDs

52
src/utils/ens.ts Normal file
View File

@ -0,0 +1,52 @@
import { gql, OperationContext, OperationResult } from 'urql'
import { fetchData } from './subgraph'
// make sure to only query for domains owned by account, so domains
// solely set by 3rd parties like *.gitcoin.eth won't show up
const UserEnsNames = gql<any>`
query UserEnsDomains($accountId: String!) {
domains(where: { resolvedAddress: $accountId, owner: $accountId }) {
name
}
}
`
const UserEnsAddress = gql<any>`
query UserEnsDomainsAddress($name: String!) {
domains(where: { name: $name }) {
resolvedAddress {
id
}
}
}
`
const ensSubgraphQueryContext: OperationContext = {
url: `https://api.thegraph.com/subgraphs/name/ensdomains/ens`,
requestPolicy: 'cache-and-network'
}
export async function getEnsName(accountId: string): Promise<string> {
const response: OperationResult<any> = await fetchData(
UserEnsNames,
{ accountId: accountId.toLowerCase() },
ensSubgraphQueryContext
)
if (!response?.data?.domains?.length) return
// Default order of response.data.domains seems to be by creation time, from oldest to newest.
// Pick the last one as that is what direct web3 calls do.
const { name } = response.data.domains.slice(-1)[0]
return name
}
export async function getEnsAddress(ensName: string): Promise<string> {
const response: OperationResult<any> = await fetchData(
UserEnsAddress,
{ name: ensName },
ensSubgraphQueryContext
)
if (!response?.data?.domains?.length) return
const { id } = response.data.domains[0].resolvedAddress
return id
}

View File

@ -1,11 +1,12 @@
import { Logger, Ocean } from '@oceanprotocol/lib'
import { TransactionReceipt } from 'web3-core'
export async function setMinterToPublisher(
ocean: Ocean,
dataTokenAddress: string,
accountId: string,
setError: (msg: string) => void
): Promise<any> {
): Promise<TransactionReceipt> {
// free pricing v3 workaround part1
const response = await ocean.OceanDispenser.cancelMinter(
dataTokenAddress,
@ -23,7 +24,7 @@ export async function setMinterToDispenser(
dataTokenAddress: string,
accountId: string,
setError: (msg: string) => void
): Promise<any> {
): Promise<TransactionReceipt> {
// free pricing v3 workaround part2
const response = await ocean.OceanDispenser.makeMinter(
dataTokenAddress,

View File

@ -76,7 +76,7 @@ export function checkIfTimeoutInPredefinedValues(
return false
}
function getAlgoithComponent(
function getAlgorithmComponent(
image: string,
containerTag: string,
entrypoint: string,
@ -94,7 +94,7 @@ function getAlgoithComponent(
}
}
function getAlgoithFileExtension(fileUrl: string): string {
function getAlgorithmFileExtension(fileUrl: string): string {
const splitedFileUrl = fileUrl.split('.')
return splitedFileUrl[splitedFileUrl.length - 1]
}
@ -203,7 +203,6 @@ export function transformPublishAlgorithmFormToMetadata(
author,
description,
tags,
dockerImage,
image,
containerTag,
entrypoint,
@ -214,12 +213,12 @@ export function transformPublishAlgorithmFormToMetadata(
): MetadataMarket {
const currentTime = toStringNoMS(new Date())
const fileUrl = typeof files !== 'string' && files[0].url
const algorithmLanguace = getAlgoithFileExtension(fileUrl)
const algorithm = getAlgoithComponent(
const algorithmLanguage = getAlgorithmFileExtension(fileUrl)
const algorithm = getAlgorithmComponent(
image,
containerTag,
entrypoint,
algorithmLanguace
algorithmLanguage
)
const metadata: MetadataMarket = {
main: {
@ -230,7 +229,7 @@ export function transformPublishAlgorithmFormToMetadata(
dateCreated: ddo ? ddo.created : currentTime,
files: typeof files !== 'string' && files,
license: 'https://market.oceanprotocol.com/terms',
algorithm: algorithm
algorithm
},
additionalInformation: {
...AssetModel.additionalInformation,
@ -242,29 +241,3 @@ export function transformPublishAlgorithmFormToMetadata(
return metadata
}
function idToName(id: number): string {
switch (id) {
case 1:
return 'eth'
case 137:
return 'polygon'
case 3:
return 'ropsten'
case 4:
return 'rinkeby'
case 1287:
return 'moonbase'
default:
return 'eth'
}
}
export function mapChainIdsToNetworkNames(chainIds: number[]): string[] {
const networkNames: string[] = []
for (let i = 0; i < chainIds.length; i++) {
const networkName = idToName(chainIds[i])
networkNames.push(networkName)
}
return networkNames
}

View File

@ -1,11 +1,10 @@
import { Profile, ProfileLink, ResponseData3Box } from '../models/Profile'
import axios, { AxiosResponse, CancelToken } from 'axios'
import jwtDecode from 'jwt-decode'
import { Logger } from '@oceanprotocol/lib'
// https://docs.3box.io/api/rest-api
const apiUri = 'https://3box.oceanprotocol.com'
const ipfsUrl = 'https://dweb.link'
const ipfsUrl = 'https://infura-ipfs.io'
function decodeProof(proofJWT: string) {
if (!proofJWT) return

View File

@ -1,15 +1,15 @@
import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql'
import { DDO } from '@oceanprotocol/lib'
import { DDO, Logger } from '@oceanprotocol/lib'
import { getUrqlClientInstance } from '../providers/UrqlProvider'
import { getOceanConfig } from './ocean'
import web3 from 'web3'
import {
AssetsPoolPrice,
AssetsPoolPrice_pools as AssetsPoolPricePools
AssetsPoolPrice_pools as AssetsPoolPricePool
} from '../@types/apollo/AssetsPoolPrice'
import {
AssetsFrePrice,
AssetsFrePrice_fixedRateExchanges as AssetsFrePriceFixedRateExchanges
AssetsFrePrice_fixedRateExchanges as AssetsFrePriceFixedRateExchange
} from '../@types/apollo/AssetsFrePrice'
import {
AssetsFreePrice,
@ -17,10 +17,20 @@ import {
} from '../@types/apollo/AssetsFreePrice'
import { AssetPreviousOrder } from '../@types/apollo/AssetPreviousOrder'
import {
HighestLiquidityAssets_pools as HighestLiquidityAssetsPools,
HighestLiquidityAssets_pools as HighestLiquidityAssetsPool,
HighestLiquidityAssets as HighestLiquidityGraphAssets
} from '../@types/apollo/HighestLiquidityAssets'
import {
PoolShares as PoolSharesList,
PoolShares_poolShares as PoolShare
} from '../@types/apollo/PoolShares'
import { BestPrice } from '../models/BestPrice'
import { OrdersData_tokenOrders as OrdersData } from '../@types/apollo/OrdersData'
export interface UserLiquidity {
price: string
oceanBalance: string
}
export interface PriceList {
[key: string]: string
@ -112,7 +122,7 @@ const PoolQuery = gql`
}
`
const AssetPoolPriceQuerry = gql`
const AssetPoolPriceQuery = gql`
query AssetPoolPrice($datatokenAddress: String) {
pools(where: { datatokenAddress: $datatokenAddress }) {
id
@ -157,6 +167,92 @@ const HighestLiquidityAssets = gql`
}
`
const TotalAccountOrders = gql`
query TotalAccountOrders($datatokenId_in: [String!]) {
tokenOrders(where: { datatokenId_in: $datatokenId_in }) {
payer {
id
}
datatokenId {
id
}
}
}
`
const UserSharesQuery = gql`
query UserSharesQuery($user: String, $pools: [String!]) {
poolShares(where: { userAddress: $user, poolId_in: $pools }) {
id
balance
userAddress {
id
}
poolId {
id
datatokenAddress
valueLocked
tokens {
tokenId {
symbol
}
}
oceanReserve
datatokenReserve
totalShares
consumePrice
spotPrice
createTime
}
}
}
`
const userPoolSharesQuery = gql`
query PoolShares($user: String) {
poolShares(where: { userAddress: $user, balance_gt: 0.001 }, first: 1000) {
id
balance
userAddress {
id
}
poolId {
id
datatokenAddress
valueLocked
tokens {
id
isDatatoken
symbol
}
oceanReserve
datatokenReserve
totalShares
consumePrice
spotPrice
createTime
}
}
}
`
const UserTokenOrders = gql`
query OrdersData($user: String!) {
tokenOrders(
orderBy: timestamp
orderDirection: desc
where: { consumer: $user }
) {
datatokenId {
address
symbol
}
timestamp
tx
}
}
`
export function getSubgraphUri(chainId: number): string {
const config = getOceanConfig(chainId)
return config.subgraphUri
@ -238,8 +334,8 @@ export async function getPreviousOrders(
}
function transformPriceToBestPrice(
frePrice: AssetsFrePriceFixedRateExchanges[],
poolPrice: AssetsPoolPricePools[],
frePrice: AssetsFrePriceFixedRateExchange[],
poolPrice: AssetsPoolPricePool[],
freePrice: AssetFreePriceDispenser[]
) {
if (poolPrice?.length > 0) {
@ -303,8 +399,8 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
assets: DDO[]
): Promise<
[
AssetsPoolPricePools[],
AssetsFrePriceFixedRateExchanges[],
AssetsPoolPricePool[],
AssetsFrePriceFixedRateExchange[],
AssetFreePriceDispenser[],
DidAndDatatokenMap
]
@ -322,8 +418,8 @@ async function getAssetsPoolsExchangesAndDatatokenMap(
chainAssetLists[ddo.chainId].push(ddo?.dataToken.toLowerCase())
}
}
let poolPriceResponse: AssetsPoolPricePools[] = []
let frePriceResponse: AssetsFrePriceFixedRateExchanges[] = []
let poolPriceResponse: AssetsPoolPricePool[] = []
let frePriceResponse: AssetsFrePriceFixedRateExchange[] = []
let freePriceResponse: AssetFreePriceDispenser[] = []
for (const chainKey in chainAssetLists) {
@ -366,8 +462,8 @@ export async function getAssetsPriceList(assets: DDO[]): Promise<PriceList> {
const priceList: PriceList = {}
const values: [
AssetsPoolPricePools[],
AssetsFrePriceFixedRateExchanges[],
AssetsPoolPricePool[],
AssetsFrePriceFixedRateExchange[],
AssetFreePriceDispenser[],
DidAndDatatokenMap
] = await getAssetsPoolsExchangesAndDatatokenMap(assets)
@ -404,7 +500,7 @@ export async function getPrice(asset: DDO): Promise<BestPrice> {
const queryContext = getQueryContext(Number(asset.chainId))
const poolPriceResponse: OperationResult<AssetsPoolPrice> = await fetchData(
AssetPoolPriceQuerry,
AssetPoolPriceQuery,
poolVariables,
queryContext
)
@ -435,7 +531,7 @@ export async function getSpotPrice(asset: DDO): Promise<number> {
const queryContext = getQueryContext(Number(asset.chainId))
const poolPriceResponse: OperationResult<AssetsPoolPrice> = await fetchData(
AssetPoolPriceQuerry,
AssetPoolPriceQuery,
poolVariables,
queryContext
)
@ -449,8 +545,8 @@ export async function getAssetsBestPrices(
const assetsWithPrice: AssetListPrices[] = []
const values: [
AssetsPoolPricePools[],
AssetsFrePriceFixedRateExchanges[],
AssetsPoolPricePool[],
AssetsFrePriceFixedRateExchange[],
AssetFreePriceDispenser[],
DidAndDatatokenMap
] = await getAssetsPoolsExchangesAndDatatokenMap(assets)
@ -460,19 +556,20 @@ export async function getAssetsBestPrices(
const freePriceResponse = values[2]
for (const ddo of assets) {
const dataToken = ddo.dataToken.toLowerCase()
const poolPrice: AssetsPoolPricePools[] = []
const frePrice: AssetsFrePriceFixedRateExchanges[] = []
const poolPrice: AssetsPoolPricePool[] = []
const frePrice: AssetsFrePriceFixedRateExchange[] = []
const freePrice: AssetFreePriceDispenser[] = []
const pool = poolPriceResponse.find(
(pool: any) => pool.datatokenAddress === dataToken
(pool: AssetsPoolPricePool) => pool.datatokenAddress === dataToken
)
pool && poolPrice.push(pool)
const fre = frePriceResponse.find(
(fre: any) => fre.datatoken.address === dataToken
(fre: AssetsFrePriceFixedRateExchange) =>
fre.datatoken.address === dataToken
)
fre && frePrice.push(fre)
const free = freePriceResponse.find(
(free: any) => free.datatoken.address === dataToken
(free: AssetFreePriceDispenser) => free.datatoken.address === dataToken
)
free && freePrice.push(free)
const bestPrice = transformPriceToBestPrice(frePrice, poolPrice, freePrice)
@ -489,22 +586,22 @@ export async function getHighestLiquidityDIDs(
chainIds: number[]
): Promise<[string, number]> {
const didList: string[] = []
let highestLiquidiyAssets: HighestLiquidityAssetsPools[] = []
let highestLiquidityAssets: HighestLiquidityAssetsPool[] = []
for (const chain of chainIds) {
const queryContext = getQueryContext(Number(chain))
const fetchedPools: OperationResult<HighestLiquidityGraphAssets, any> =
await fetchData(HighestLiquidityAssets, null, queryContext)
highestLiquidiyAssets = highestLiquidiyAssets.concat(
highestLiquidityAssets = highestLiquidityAssets.concat(
fetchedPools.data.pools
)
}
highestLiquidiyAssets
highestLiquidityAssets
.sort((a, b) => a.oceanReserve - b.oceanReserve)
.reverse()
for (let i = 0; i < highestLiquidiyAssets.length; i++) {
if (!highestLiquidiyAssets[i].datatokenAddress) continue
for (let i = 0; i < highestLiquidityAssets.length; i++) {
if (!highestLiquidityAssets[i].datatokenAddress) continue
const did = web3.utils
.toChecksumAddress(highestLiquidiyAssets[i].datatokenAddress)
.toChecksumAddress(highestLiquidityAssets[i].datatokenAddress)
.replace('0x', 'did:op:')
didList.push(did)
}
@ -515,3 +612,113 @@ export async function getHighestLiquidityDIDs(
.replace(/(did:op:)/g, '0x')
return [searchDids, didList.length]
}
export async function getAccountNumberOfOrders(
assets: DDO[],
chainIds: number[]
): Promise<number> {
const datatokens: string[] = []
assets.forEach((ddo) => {
datatokens.push(ddo?.dataToken?.toLowerCase())
})
const queryVariables = {
datatokenId_in: datatokens
}
const results = await fetchDataForMultipleChains(
TotalAccountOrders,
queryVariables,
chainIds
)
let numberOfOrders = 0
for (const result of results) {
numberOfOrders += result?.tokenOrders?.length
}
return numberOfOrders
}
export function calculateUserLiquidity(poolShare: PoolShare): number {
const ocean =
(poolShare.balance / poolShare.poolId.totalShares) *
poolShare.poolId.oceanReserve
const datatokens =
(poolShare.balance / poolShare.poolId.totalShares) *
poolShare.poolId.datatokenReserve
const totalLiquidity = ocean + datatokens * poolShare.poolId.consumePrice
return totalLiquidity
}
export async function getAccountLiquidityInOwnAssets(
accountId: string,
chainIds: number[],
pools: string[]
): Promise<UserLiquidity> {
const queryVariables = {
user: accountId.toLowerCase(),
pools: pools
}
const results: PoolSharesList[] = await fetchDataForMultipleChains(
UserSharesQuery,
queryVariables,
chainIds
)
let totalLiquidity = 0
let totalOceanLiquidity = 0
for (const result of results) {
for (const poolShare of result.poolShares) {
const userShare = poolShare.balance / poolShare.poolId.totalShares
const userBalance = userShare * poolShare.poolId.oceanReserve
totalOceanLiquidity += userBalance
const poolLiquidity = calculateUserLiquidity(poolShare)
totalLiquidity += poolLiquidity
}
}
return {
price: totalLiquidity.toString(),
oceanBalance: totalOceanLiquidity.toString()
}
}
export async function getPoolSharesData(
accountId: string,
chainIds: number[]
): Promise<PoolShare[]> {
const variables = { user: accountId?.toLowerCase() }
const data: PoolShare[] = []
const result = await fetchDataForMultipleChains(
userPoolSharesQuery,
variables,
chainIds
)
for (let i = 0; i < result.length; i++) {
result[i].poolShares.forEach((poolShare: PoolShare) => {
data.push(poolShare)
})
}
return data
}
export async function getUserTokenOrders(
accountId: string,
chainIds: number[]
): Promise<OrdersData[]> {
const data: OrdersData[] = []
const variables = { user: accountId?.toLowerCase() }
try {
const tokenOrders = await fetchDataForMultipleChains(
UserTokenOrders,
variables,
chainIds
)
for (let i = 0; i < tokenOrders?.length; i++) {
tokenOrders[i].tokenOrders.forEach((tokenOrder: OrdersData) => {
data.push(tokenOrder)
})
}
return data
} catch (error) {
Logger.error(error.message)
}
}

View File

@ -23,8 +23,8 @@
"link": "/publish"
},
{
"name": "History",
"link": "/history"
"name": "Profile",
"link": "/profile"
}
]
}