diff --git a/_redirects b/_redirects index 3b85ef2de..db198413a 100644 --- a/_redirects +++ b/_redirects @@ -1 +1,2 @@ -/asset/* /asset/index.html 200 \ No newline at end of file +/asset/* /asset/index.html 200 +/profile/* /profile/index.html 200 \ No newline at end of file diff --git a/content/pages/history.json b/content/pages/history.json index 57df3eb16..8dfbf2a8a 100644 --- a/content/pages/history.json +++ b/content/pages/history.json @@ -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." } diff --git a/content/pages/profile.json b/content/pages/profile.json new file mode 100644 index 000000000..b1d2c8362 --- /dev/null +++ b/content/pages/profile.json @@ -0,0 +1,4 @@ +{ + "title": "Account", + "description": "Find the data sets and jobs that you previously accessed." +} diff --git a/content/site.json b/content/site.json index 6b6556de2..7cef28e78 100644 --- a/content/site.json +++ b/content/site.json @@ -12,8 +12,8 @@ "link": "/publish" }, { - "name": "History", - "link": "/history" + "name": "Profile", + "link": "/profile" } ], "warning": { diff --git a/gatsby-node.js b/gatsby-node.js index 86b1e751d..cb611c715 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -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) } } diff --git a/package-lock.json b/package-lock.json index 4574eb4f3..153f41abd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 00d7cac9c..318ef028c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/atoms/Blockies.module.css b/src/components/atoms/Blockies.module.css new file mode 100644 index 000000000..928123b18 --- /dev/null +++ b/src/components/atoms/Blockies.module.css @@ -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; +} diff --git a/src/components/atoms/Blockies.tsx b/src/components/atoms/Blockies.tsx new file mode 100644 index 000000000..4a02becbe --- /dev/null +++ b/src/components/atoms/Blockies.tsx @@ -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 ( + + ) +} diff --git a/src/components/atoms/Copy.module.css b/src/components/atoms/Copy.module.css new file mode 100644 index 000000000..4ba29b771 --- /dev/null +++ b/src/components/atoms/Copy.module.css @@ -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); +} diff --git a/src/components/atoms/Copy.tsx b/src/components/atoms/Copy.tsx new file mode 100644 index 000000000..d0693963f --- /dev/null +++ b/src/components/atoms/Copy.tsx @@ -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 ( + setIsCopied(true)} + className={`${styles.button} ${isCopied ? styles.copied : ''}`} + > + + + ) +} diff --git a/src/components/atoms/Publisher/ProfileDetails.module.css b/src/components/atoms/Publisher/ProfileDetails.module.css deleted file mode 100644 index a0e85bea5..000000000 --- a/src/components/atoms/Publisher/ProfileDetails.module.css +++ /dev/null @@ -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); -} diff --git a/src/components/atoms/Publisher/ProfileDetails.tsx b/src/components/atoms/Publisher/ProfileDetails.tsx deleted file mode 100644 index 7f4be381b..000000000 --- a/src/components/atoms/Publisher/ProfileDetails.tsx +++ /dev/null @@ -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 ( - <> -
-
- {profile?.image && ( -
- -
- )} -

- {profile?.emoji} {profile?.name} -

- - - {account} - -
- - {profile?.description && ( -

{profile?.description}

- )} - -
-
- Profile data from{' '} - - 3Box Hub - -
- - ) -} diff --git a/src/components/atoms/Publisher/index.module.css b/src/components/atoms/Publisher/index.module.css index 71a0287b8..93592f599 100644 --- a/src/components/atoms/Publisher/index.module.css +++ b/src/components/atoms/Publisher/index.module.css @@ -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; -} diff --git a/src/components/atoms/Publisher/index.tsx b/src/components/atoms/Publisher/index.tsx index 8fac6942c..734fd1e02 100644 --- a/src/components/atoms/Publisher/index.tsx +++ b/src/components/atoms/Publisher/index.tsx @@ -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() - const [name, setName] = useState() + const [name, setName] = useState(accountTruncate(account)) + const [accountEns, setAccountEns] = useState() 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({ ) : ( <> {name} -
- {' — '} - {profile && ( - - } - > - - Profile - - - )} - {showAdd && } - - Explorer - -
+ {showAdd && } )} diff --git a/src/components/atoms/Tabs.module.css b/src/components/atoms/Tabs.module.css index 3d76fc0d6..f138f9458 100644 --- a/src/components/atoms/Tabs.module.css +++ b/src/components/atoms/Tabs.module.css @@ -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); + } } diff --git a/src/components/atoms/Tooltip.module.css b/src/components/atoms/Tooltip.module.css index 97bcbda11..131a7cea5 100644 --- a/src/components/atoms/Tooltip.module.css +++ b/src/components/atoms/Tooltip.module.css @@ -9,6 +9,10 @@ font-size: var(--font-size-small); } +.content p { + margin: 0; +} + .icon { width: 1em; height: 1em; diff --git a/src/components/atoms/Tooltip.tsx b/src/components/atoms/Tooltip.tsx index 1719ff0a0..08a5039a2 100644 --- a/src/components/atoms/Tooltip.tsx +++ b/src/components/atoms/Tooltip.tsx @@ -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) diff --git a/src/components/molecules/AssetListTitle.tsx b/src/components/molecules/AssetListTitle.tsx index 9fb6a7535..fe6c67bfe 100644 --- a/src/components/molecules/AssetListTitle.tsx +++ b/src/components/molecules/AssetListTitle.tsx @@ -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 (

- {assetTitle} + {assetTitle}

) } diff --git a/src/components/molecules/AssetTeaser.tsx b/src/components/molecules/AssetTeaser.tsx index 2543e1ba1..b1b249642 100644 --- a/src/components/molecules/AssetTeaser.tsx +++ b/src/components/molecules/AssetTeaser.tsx @@ -13,11 +13,13 @@ import { BestPrice } from '../../models/BestPrice' declare type AssetTeaserProps = { ddo: DDO price: BestPrice + noPublisher?: boolean } const AssetTeaser: React.FC = ({ ddo, - price + price, + noPublisher }: AssetTeaserProps) => { const { attributes } = ddo.findServiceByType('metadata') const { name, type } = attributes.main @@ -34,7 +36,9 @@ const AssetTeaser: React.FC = ({

{name}

- + {!noPublisher && ( + + )} { - 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 ( +
+ {icon && icon} + {value} +
+ + {label}{' '} + {tooltip && ( + } + className={styles.tooltip} + /> + )} + + + ) +} diff --git a/src/components/molecules/PoolTransactions/Title.tsx b/src/components/molecules/PoolTransactions/Title.tsx index 5305c34f7..bd9fcf5a7 100644 --- a/src/components/molecules/PoolTransactions/Title.tsx +++ b/src/components/molecules/PoolTransactions/Title.tsx @@ -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) diff --git a/src/components/molecules/PoolTransactions/index.tsx b/src/components/molecules/PoolTransactions/index.tsx index 24cc616d2..2ba87f53f 100644 --- a/src/components/molecules/PoolTransactions/index.tsx +++ b/src/components/molecules/PoolTransactions/index.tsx @@ -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 + return } }, { @@ -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() - const [isLoading, setIsLoading] = useState(false) + const [transactions, setTransactions] = useState() + const [isLoading, setIsLoading] = useState(true) const { chainIds } = useUserPreferences() const { appConfig } = useSiteMetadata() const [dataFetchInterval, setDataFetchInterval] = useState() const [data, setData] = useState() - 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 ? (
= 4 : logs?.length >= 9} + pagination={ + minimal ? transactions?.length >= 4 : transactions?.length >= 9 + } paginationPerPage={minimal ? 5 : 10} /> ) : ( diff --git a/src/components/molecules/Wallet/Account.module.css b/src/components/molecules/Wallet/Account.module.css index 0a4d0d6be..1dbce49f8 100644 --- a/src/components/molecules/Wallet/Account.module.css +++ b/src/components/molecules/Wallet/Account.module.css @@ -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) { diff --git a/src/components/molecules/Wallet/Account.tsx b/src/components/molecules/Wallet/Account.tsx index 7b3be33fc..e95430306 100644 --- a/src/components/molecules/Wallet/Account.tsx +++ b/src/components/molecules/Wallet/Account.tsx @@ -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 ( - - ) -} +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) { // 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()} > - + - {accountTruncate(accountId)} + {accountTruncate(accountEns || accountId)}
- - ) : ( -
Please connect your Web3 wallet.
- ) -} diff --git a/src/components/pages/History/Downloads.tsx b/src/components/pages/History/Downloads.tsx deleted file mode 100644 index f12e08f69..000000000 --- a/src/components/pages/History/Downloads.tsx +++ /dev/null @@ -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 - } - }, - { - name: 'Network', - selector: function getNetwork(row: DownloadedAssets) { - return - } - }, - { - name: 'Datatoken', - selector: function getTitleRow(row: DownloadedAssets) { - return row.dtSymbol - } - }, - { - name: 'Time', - selector: function getTimeRow(row: DownloadedAssets) { - return
- ) : ( -
Please connect your Web3 wallet.
- ) -} diff --git a/src/components/pages/History/PoolShares.tsx b/src/components/pages/History/PoolShares.tsx deleted file mode 100644 index 63cf4b0e9..000000000 --- a/src/components/pages/History/PoolShares.tsx +++ /dev/null @@ -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 ( -
- - - -
- ) -} - -const columns = [ - { - name: 'Data Set', - selector: function getAssetRow(row: Asset) { - const did = web3.utils - .toChecksumAddress(row.poolShare.poolId.datatokenAddress) - .replace('0x', 'did:op:') - return - }, - grow: 2 - }, - { - name: 'Network', - selector: function getNetwork(row: Asset) { - return - } - }, - { - name: 'Datatoken', - selector: function getSymbol(row: Asset) { - return - } - }, - { - name: 'Your Liquidity', - selector: function getAssetRow(row: Asset) { - return - }, - right: true - }, - { - name: 'Pool Liquidity', - selector: function getAssetRow(row: Asset) { - return - }, - right: true - } -] - -export default function PoolShares(): ReactElement { - const { accountId } = useWeb3() - const [assets, setAssets] = useState() - const [loading, setLoading] = useState(false) - const [data, setData] = useState() - const [dataFetchInterval, setDataFetchInterval] = useState() - 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 ? ( -
- ) : ( -
Please connect your Web3 wallet.
- ) -} diff --git a/src/components/pages/History/PublishedList.tsx b/src/components/pages/History/PublishedList.tsx deleted file mode 100644 index 536f161ad..000000000 --- a/src/components/pages/History/PublishedList.tsx +++ /dev/null @@ -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() - const [isLoading, setIsLoading] = useState(false) - const [page, setPage] = useState(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 ? ( - { - setPage(newPage) - }} - /> - ) : ( -
Please connect your Web3 wallet.
- ) -} diff --git a/src/components/pages/History/index.tsx b/src/components/pages/History/index.tsx deleted file mode 100644 index aa0cfe2b8..000000000 --- a/src/components/pages/History/index.tsx +++ /dev/null @@ -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: - }, - { - title: 'Pool Shares', - content: - }, - { - title: 'Pool Transactions', - content: - }, - { - title: 'Downloads', - content: - }, - { - title: 'Compute Jobs', - content: ( - - - - ) - } -] - -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 ( -
- -
- ) -} diff --git a/src/components/pages/Profile/Header/Account.module.css b/src/components/pages/Profile/Header/Account.module.css new file mode 100644 index 000000000..2c58d49ef --- /dev/null +++ b/src/components/pages/Profile/Header/Account.module.css @@ -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); +} diff --git a/src/components/pages/Profile/Header/Account.tsx b/src/components/pages/Profile/Header/Account.tsx new file mode 100644 index 000000000..084526671 --- /dev/null +++ b/src/components/pages/Profile/Header/Account.tsx @@ -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 ( +
+
+ {profile?.image ? ( + + ) : accountId ? ( + + ) : ( + + )} +
+ +
+

{profile?.name}

+ {accountId && ( + + {profile?.accountEns || accountId} + + )} +

+ {accountId && + chainIds.map((value) => ( + + + + ))} +

+
+
+ ) +} diff --git a/src/components/atoms/Publisher/PublisherLinks.module.css b/src/components/pages/Profile/Header/PublisherLinks.module.css similarity index 69% rename from src/components/atoms/Publisher/PublisherLinks.module.css rename to src/components/pages/Profile/Header/PublisherLinks.module.css index fb52e55ef..7df4ca3d1 100644 --- a/src/components/atoms/Publisher/PublisherLinks.module.css +++ b/src/components/pages/Profile/Header/PublisherLinks.module.css @@ -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'; } diff --git a/src/components/atoms/Publisher/PublisherLinks.tsx b/src/components/pages/Profile/Header/PublisherLinks.tsx similarity index 63% rename from src/components/atoms/Publisher/PublisherLinks.tsx rename to src/components/pages/Profile/Header/PublisherLinks.tsx index 46ec0cba4..2c97782d7 100644 --- a/src/components/atoms/Publisher/PublisherLinks.tsx +++ b/src/components/pages/Profile/Header/PublisherLinks.tsx @@ -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 ( -
+
{' — '} - {links?.map((link: ProfileLink) => { + {profile?.links?.map((link) => { const href = link.name === 'Twitter' ? `https://twitter.com/${link.value}` diff --git a/src/components/pages/Profile/Header/Stats.module.css b/src/components/pages/Profile/Header/Stats.module.css new file mode 100644 index 000000000..67a6a0c7c --- /dev/null +++ b/src/components/pages/Profile/Header/Stats.module.css @@ -0,0 +1,6 @@ +.stats { + display: grid; + gap: var(--spacer); + grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); + margin-top: var(--spacer); +} diff --git a/src/components/pages/Profile/Header/Stats.tsx b/src/components/pages/Profile/Header/Stats.tsx new file mode 100644 index 000000000..163394746 --- /dev/null +++ b/src/components/pages/Profile/Header/Stats.tsx @@ -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 { + 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() + 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 ( +
+ + } + /> + } + /> + + + +
+ ) +} diff --git a/src/components/pages/Profile/Header/index.module.css b/src/components/pages/Profile/Header/index.module.css new file mode 100644 index 000000000..c9c8a33c3 --- /dev/null +++ b/src/components/pages/Profile/Header/index.module.css @@ -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); +} diff --git a/src/components/pages/Profile/Header/index.tsx b/src/components/pages/Profile/Header/index.tsx new file mode 100644 index 000000000..95f05fae9 --- /dev/null +++ b/src/components/pages/Profile/Header/index.tsx @@ -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 ( + + {text} + + ) +} + +export default function AccountHeader({ + accountId +}: { + accountId: string +}): ReactElement { + const { profile } = useProfile() + const [isShowMore, setIsShowMore] = useState(false) + + const toogleShowMore = () => { + setIsShowMore(!isShowMore) + } + + return ( +
+
+ + +
+ +
+ + {isDescriptionTextClamped() ? ( + + + + ) : ( + '' + )} + {profile?.links?.length > 0 && ( + + )} +
+
+ Profile data from{' '} + {profile?.accountEns && ( + <> + {' '} + &{' '} + + )} + +
+
+ ) +} diff --git a/src/components/pages/History/ComputeJobs/Details.module.css b/src/components/pages/Profile/History/ComputeJobs/Details.module.css similarity index 95% rename from src/components/pages/History/ComputeJobs/Details.module.css rename to src/components/pages/Profile/History/ComputeJobs/Details.module.css index 785537c38..53a32197e 100644 --- a/src/components/pages/History/ComputeJobs/Details.module.css +++ b/src/components/pages/Profile/History/ComputeJobs/Details.module.css @@ -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); diff --git a/src/components/pages/History/ComputeJobs/Details.tsx b/src/components/pages/Profile/History/ComputeJobs/Details.tsx similarity index 83% rename from src/components/pages/History/ComputeJobs/Details.tsx rename to src/components/pages/Profile/History/ComputeJobs/Details.tsx index 1cc5fef97..3620b2f99 100644 --- a/src/components/pages/History/ComputeJobs/Details.tsx +++ b/src/components/pages/Profile/History/ComputeJobs/Details.tsx @@ -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, diff --git a/src/components/pages/History/ComputeJobs/Results.module.css b/src/components/pages/Profile/History/ComputeJobs/Results.module.css similarity index 100% rename from src/components/pages/History/ComputeJobs/Results.module.css rename to src/components/pages/Profile/History/ComputeJobs/Results.module.css diff --git a/src/components/pages/History/ComputeJobs/Results.tsx b/src/components/pages/Profile/History/ComputeJobs/Results.tsx similarity index 89% rename from src/components/pages/History/ComputeJobs/Results.tsx rename to src/components/pages/Profile/History/ComputeJobs/Results.tsx index 7d5f04941..9c2d64f75 100644 --- a/src/components/pages/History/ComputeJobs/Results.tsx +++ b/src/components/pages/Profile/History/ComputeJobs/Results.tsx @@ -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` diff --git a/src/components/pages/History/ComputeJobs/index.module.css b/src/components/pages/Profile/History/ComputeJobs/index.module.css similarity index 100% rename from src/components/pages/History/ComputeJobs/index.module.css rename to src/components/pages/Profile/History/ComputeJobs/index.module.css diff --git a/src/components/pages/Profile/History/ComputeJobs/index.tsx b/src/components/pages/Profile/History/ComputeJobs/index.tsx new file mode 100644 index 000000000..e43d97c3d --- /dev/null +++ b/src/components/pages/Profile/History/ComputeJobs/index.tsx @@ -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
{children}
+} + +const columns = [ + { + name: 'Data Set', + selector: function getAssetRow(row: ComputeJobMetaData) { + return ( + + {row.assetName} + + ) + } + }, + { + name: 'Network', + selector: function getNetwork(row: ComputeJobMetaData) { + return + } + }, + { + name: 'Created', + selector: function getTimeRow(row: ComputeJobMetaData) { + return
+ + ) : ( +
Please connect your Web3 wallet.
+ ) +} diff --git a/src/components/pages/Profile/History/Downloads.tsx b/src/components/pages/Profile/History/Downloads.tsx new file mode 100644 index 000000000..ef06b9f01 --- /dev/null +++ b/src/components/pages/Profile/History/Downloads.tsx @@ -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 + } + }, + { + name: 'Network', + selector: function getNetwork(row: DownloadedAsset) { + return + } + }, + { + name: 'Datatoken', + selector: function getTitleRow(row: DownloadedAsset) { + return row.dtSymbol + } + }, + { + name: 'Time', + selector: function getTimeRow(row: DownloadedAsset) { + return
+ ) : ( +
Please connect your Web3 wallet.
+ ) +} diff --git a/src/components/pages/History/PoolShares.module.css b/src/components/pages/Profile/History/PoolShares.module.css similarity index 79% rename from src/components/pages/History/PoolShares.module.css rename to src/components/pages/Profile/History/PoolShares.module.css index 9cd00bbe6..4dcf96482 100644 --- a/src/components/pages/History/PoolShares.module.css +++ b/src/components/pages/Profile/History/PoolShares.module.css @@ -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; } diff --git a/src/components/pages/Profile/History/PoolShares.tsx b/src/components/pages/Profile/History/PoolShares.tsx new file mode 100644 index 000000000..80c1a1a98 --- /dev/null +++ b/src/components/pages/Profile/History/PoolShares.tsx @@ -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 ( +
+ + + +
+ ) +} + +const columns = [ + { + name: 'Data Set', + selector: function getAssetRow(row: Asset) { + return + }, + grow: 2 + }, + { + name: 'Network', + selector: function getNetwork(row: Asset) { + return + } + }, + { + name: 'Datatoken', + selector: function getSymbol(row: Asset) { + return + } + }, + { + name: 'Liquidity', + selector: function getAssetRow(row: Asset) { + return + }, + right: true + }, + { + name: 'Pool Liquidity', + selector: function getAssetRow(row: Asset) { + return + }, + 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() + const [loading, setLoading] = useState(false) + const [dataFetchInterval, setDataFetchInterval] = useState() + + 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 ? ( +
+ ) : ( +
Please connect your Web3 wallet.
+ ) +} diff --git a/src/components/pages/Profile/History/PublishedList.module.css b/src/components/pages/Profile/History/PublishedList.module.css new file mode 100644 index 000000000..4ef16e734 --- /dev/null +++ b/src/components/pages/Profile/History/PublishedList.module.css @@ -0,0 +1,7 @@ +.filters { + margin-top: -1rem; +} + +.assets { + margin-top: calc(var(--spacer) / 3); +} diff --git a/src/components/pages/Profile/History/PublishedList.tsx b/src/components/pages/Profile/History/PublishedList.tsx new file mode 100644 index 000000000..21995ef76 --- /dev/null +++ b/src/components/pages/Profile/History/PublishedList.tsx @@ -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() + const [isLoading, setIsLoading] = useState(false) + const [page, setPage] = useState(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 ? ( + <> + {/* */} + { + setPage(newPage) + }} + className={styles.assets} + noPublisher + /> + + ) : ( +
Please connect your Web3 wallet.
+ ) +} diff --git a/src/components/pages/History/index.module.css b/src/components/pages/Profile/History/index.module.css similarity index 98% rename from src/components/pages/History/index.module.css rename to src/components/pages/Profile/History/index.module.css index 7bfacd3ef..75db8b925 100644 --- a/src/components/pages/History/index.module.css +++ b/src/components/pages/Profile/History/index.module.css @@ -16,7 +16,7 @@ } } -.content { +.tabs { margin-top: var(--spacer); background-color: var(--background-body); } diff --git a/src/components/pages/Profile/History/index.tsx b/src/components/pages/Profile/History/index.tsx new file mode 100644 index 000000000..dc817fb50 --- /dev/null +++ b/src/components/pages/Profile/History/index.tsx @@ -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: + }, + { + title: 'Pool Shares', + content: + }, + { + title: 'Pool Transactions', + content: + }, + { + title: 'Downloads', + content: + } + ] + const computeTab: HistoryTab = { + title: 'Compute Jobs', + content: ( + + + + ) + } + 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 ( + + ) +} diff --git a/src/components/pages/Profile/index.tsx b/src/components/pages/Profile/index.tsx new file mode 100644 index 000000000..d4eafd450 --- /dev/null +++ b/src/components/pages/Profile/index.tsx @@ -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 ( + <> + + + + ) +} diff --git a/src/components/templates/Page.tsx b/src/components/templates/Page.tsx index 1672d4dbf..b91717999 100644 --- a/src/components/templates/Page.tsx +++ b/src/components/templates/Page.tsx @@ -5,7 +5,7 @@ import Container from '../atoms/Container' export interface PageProps { children: ReactNode - title: string + title?: string uri: string description?: string noPageHeader?: boolean diff --git a/src/components/templates/Search/filterService.module.css b/src/components/templates/Search/Filters.module.css similarity index 94% rename from src/components/templates/Search/filterService.module.css rename to src/components/templates/Search/Filters.module.css index 04d0151ef..18b4ee0fd 100644 --- a/src/components/templates/Search/filterService.module.css +++ b/src/components/templates/Search/Filters.module.css @@ -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); diff --git a/src/components/templates/Search/filterService.tsx b/src/components/templates/Search/Filters.tsx similarity index 99% rename from src/components/templates/Search/filterService.tsx rename to src/components/templates/Search/Filters.tsx index a75252470..dd91e789e 100644 --- a/src/components/templates/Search/filterService.tsx +++ b/src/components/templates/Search/Filters.tsx @@ -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) diff --git a/src/components/templates/Search/index.tsx b/src/components/templates/Search/index.tsx index a8fc13bee..4a5d18bdd 100644 --- a/src/components/templates/Search/index.tsx +++ b/src/components/templates/Search/index.tsx @@ -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({ <>
- - - - ) -} - -export const contentQuery = graphql` - query HistoryPageQuery { - content: allFile(filter: { relativePath: { eq: "pages/history.json" } }) { - edges { - node { - childPagesJson { - title - description - } - } - } - } - } -` diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx new file mode 100644 index 000000000..160a43261 --- /dev/null +++ b/src/pages/profile/index.tsx @@ -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() + const [finalAccountEns, setFinalAccountEns] = useState() + + // 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 ( + + + + + + ) +} + +export const contentQuery = graphql` + query ProfilePageQuery { + content: allFile(filter: { relativePath: { eq: "pages/profile.json" } }) { + edges { + node { + childPagesJson { + title + description + } + } + } + } + } +` diff --git a/src/providers/Profile.tsx b/src/providers/Profile.tsx new file mode 100644 index 000000000..8cb2689aa --- /dev/null +++ b/src/providers/Profile.tsx @@ -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() + + // + // 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() + + 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() + const [isPoolSharesLoading, setIsPoolSharesLoading] = useState(false) + const [poolSharesInterval, setPoolSharesInterval] = useState() + + 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() + const [assetsTotal, setAssetsTotal] = useState(0) + // const [assetsWithPrices, setAssetsWithPrices] = useState() + + 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() + const [downloadsTotal, setDownloadsTotal] = useState(0) + const [isDownloadsLoading, setIsDownloadsLoading] = useState() + const [downloadsInterval, setDownloadsInterval] = useState() + + 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 ( + + {children} + + ) +} + +// Helper hook to access the provider values +const useProfile = (): ProfileProviderValue => useContext(ProfileContext) + +export { ProfileProvider, useProfile, ProfileProviderValue, ProfileContext } +export default ProfileProvider diff --git a/src/providers/Web3.tsx b/src/providers/Web3.tsx index 09eca99eb..5f76e8fe3 100644 --- a/src/providers/Web3.tsx +++ b/src/providers/Web3.tsx @@ -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() const [isTestnet, setIsTestnet] = useState() const [accountId, setAccountId] = useState() + const [accountEns, setAccountEns] = useState() const [web3Loading, setWeb3Loading] = useState(true) const [balance, setBalance] = useState({ 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, diff --git a/src/utils/aquarius.ts b/src/utils/aquarius.ts index fa88d630f..1841ac58b 100644 --- a/src/utils/aquarius.ts +++ b/src/utils/aquarius.ts @@ -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 = 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> = 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 { + 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 { + 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 { + 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) + } +} diff --git a/src/utils/compute.ts b/src/utils/compute.ts index fe37f0b7e..dbc5c518a 100644 --- a/src/utils/compute.ts +++ b/src/utils/compute.ts @@ -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 { + 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 { + 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 { + 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 { + 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 diff --git a/src/utils/ens.ts b/src/utils/ens.ts new file mode 100644 index 000000000..9f2807bc3 --- /dev/null +++ b/src/utils/ens.ts @@ -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` + query UserEnsDomains($accountId: String!) { + domains(where: { resolvedAddress: $accountId, owner: $accountId }) { + name + } + } +` + +const UserEnsAddress = gql` + 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 { + const response: OperationResult = 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 { + const response: OperationResult = await fetchData( + UserEnsAddress, + { name: ensName }, + ensSubgraphQueryContext + ) + if (!response?.data?.domains?.length) return + const { id } = response.data.domains[0].resolvedAddress + return id +} diff --git a/src/utils/freePrice.ts b/src/utils/freePrice.ts index 68e6de649..98645e4de 100644 --- a/src/utils/freePrice.ts +++ b/src/utils/freePrice.ts @@ -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 { +): Promise { // 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 { +): Promise { // free pricing v3 workaround part2 const response = await ocean.OceanDispenser.makeMinter( dataTokenAddress, diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index 7e4578b8b..0a8ebada4 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -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 -} diff --git a/src/utils/profile.ts b/src/utils/profile.ts index f78d2c71b..9917a2588 100644 --- a/src/utils/profile.ts +++ b/src/utils/profile.ts @@ -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 diff --git a/src/utils/subgraph.ts b/src/utils/subgraph.ts index 1b77f9685..314ebc9db 100644 --- a/src/utils/subgraph.ts +++ b/src/utils/subgraph.ts @@ -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 { 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 { const queryContext = getQueryContext(Number(asset.chainId)) const poolPriceResponse: OperationResult = await fetchData( - AssetPoolPriceQuerry, + AssetPoolPriceQuery, poolVariables, queryContext ) @@ -435,7 +531,7 @@ export async function getSpotPrice(asset: DDO): Promise { const queryContext = getQueryContext(Number(asset.chainId)) const poolPriceResponse: OperationResult = 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 = 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 { + 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 { + 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 { + 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 { + 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) + } +} diff --git a/tests/unit/__fixtures__/siteMetadata.json b/tests/unit/__fixtures__/siteMetadata.json index 37d545223..a00cd86a2 100644 --- a/tests/unit/__fixtures__/siteMetadata.json +++ b/tests/unit/__fixtures__/siteMetadata.json @@ -23,8 +23,8 @@ "link": "/publish" }, { - "name": "History", - "link": "/history" + "name": "Profile", + "link": "/profile" } ] }