1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-06-26 03:06:49 +02:00

add total user liquidity, new Profile provider (#841)

* label renaming, add total user liquidity

* new useProfile provider

* centralize pool shares fetching

* add some assets fetching to profile provider

* move 3box profile fetching, check passed accountId

* cancel token fixes

* remove publisher on published assets list

* more cancel token fixes

* prevent asset name double fetching in pool shares

* prevent asset name double fetching in downloads

* prevent asset name double fetching in pool transactions

* more cancel token fixes

* refetch crash fix

* another pool shares refetch fix

* pool transactions data flow refactor

* Add total downloads, speed up downloads fetching (#849)

* add total downloads

* replace multiple retrieveDDO with one single request

* getAssetsFromDidList() helper

* fix mixed up timestamps

* data structure based on tokenOrders

* add logging

* add tooltip to downloads, small NumberUnit refactor

* safeguard against passed empty didList

* deal with plural/singular in labels
This commit is contained in:
Matthias Kretschmann 2021-09-13 16:39:32 +02:00 committed by GitHub
parent 5a336bd699
commit 032606e61c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 800 additions and 435 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
font-size: var(--font-size-h4); font-size: var(--font-size-h4);
color: var(--font-color-heading); color: var(--font-color-heading);
margin-left: 0;
} }
.number { .number {
@ -46,3 +47,9 @@
background: var(--brand-white); background: var(--brand-white);
border: 0.1rem solid var(--brand-pink); border: 0.1rem solid var(--brand-pink);
} }
.tooltip svg {
width: 0.8em !important;
height: 0.8em !important;
margin-left: 0;
}

View File

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

View File

@ -68,6 +68,7 @@ export default function Title({ row }: { row: PoolTransaction }): ReactElement {
useEffect(() => { useEffect(() => {
if (!locale || !row) return if (!locale || !row) return
async function init() { async function init() {
const title = await getTitle(row, locale) const title = await getTitle(row, locale)
setTitle(title) setTitle(title)

View File

@ -1,4 +1,4 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useCallback, useEffect, useState } from 'react'
import Time from '../../atoms/Time' import Time from '../../atoms/Time'
import Table from '../../atoms/Table' import Table from '../../atoms/Table'
import AssetTitle from '../AssetListTitle' import AssetTitle from '../AssetListTitle'
@ -10,9 +10,10 @@ import { fetchDataForMultipleChains } from '../../../utils/subgraph'
import { useSiteMetadata } from '../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
import NetworkName from '../../atoms/NetworkName' import NetworkName from '../../atoms/NetworkName'
import { retrieveDDO } from '../../../utils/aquarius' import { retrieveDDO } from '../../../utils/aquarius'
import axios from 'axios' import axios, { CancelToken } from 'axios'
import Title from './Title' import Title from './Title'
import styles from './index.module.css' import styles from './index.module.css'
import { DDO, Logger } from '@oceanprotocol/lib'
const REFETCH_INTERVAL = 20000 const REFETCH_INTERVAL = 20000
@ -75,6 +76,7 @@ export interface Datatoken {
export interface PoolTransaction extends TransactionHistoryPoolTransactions { export interface PoolTransaction extends TransactionHistoryPoolTransactions {
networkId: number networkId: number
ddo: DDO
} }
const columns = [ const columns = [
@ -87,11 +89,7 @@ const columns = [
{ {
name: 'Data Set', name: 'Data Set',
selector: function getAssetRow(row: PoolTransaction) { selector: function getAssetRow(row: PoolTransaction) {
const did = web3.utils return <AssetTitle ddo={row.ddo} />
.toChecksumAddress(row.poolAddress.datatokenAddress)
.replace('0x', 'did:op:')
return <AssetTitle did={did} />
} }
}, },
{ {
@ -131,14 +129,14 @@ export default function PoolTransactions({
minimal?: boolean minimal?: boolean
accountId: string accountId: string
}): ReactElement { }): ReactElement {
const [logs, setLogs] = useState<PoolTransaction[]>() const [transactions, setTransactions] = useState<PoolTransaction[]>()
const [isLoading, setIsLoading] = useState<boolean>(false) const [isLoading, setIsLoading] = useState<boolean>(true)
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { appConfig } = useSiteMetadata() const { appConfig } = useSiteMetadata()
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>() const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
const [data, setData] = useState<PoolTransaction[]>() const [data, setData] = useState<PoolTransaction[]>()
async function fetchPoolTransactionData() { const getPoolTransactionData = useCallback(async () => {
const variables = { const variables = {
user: accountId?.toLowerCase(), user: accountId?.toLowerCase(),
pool: poolAddress?.toLowerCase() pool: poolAddress?.toLowerCase()
@ -159,71 +157,94 @@ export default function PoolTransactions({
if (JSON.stringify(data) !== JSON.stringify(transactions)) { if (JSON.stringify(data) !== JSON.stringify(transactions)) {
setData(transactions) setData(transactions)
} }
} }, [accountId, chainIds, data, poolAddress, poolChainId])
function refetchPoolTransactions() { const getPoolTransactions = useCallback(
if (!dataFetchInterval) { async (cancelToken: CancelToken) => {
setDataFetchInterval( if (!data) return
setInterval(function () {
fetchPoolTransactionData() const poolTransactions: PoolTransaction[] = []
}, REFETCH_INTERVAL)
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(() => { 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 () => { return () => {
clearInterval(dataFetchInterval) clearInterval(dataFetchInterval)
} }
}, [dataFetchInterval]) }, [getPoolTransactionData, dataFetchInterval, appConfig.metadataCacheUri])
//
// Transform to final transactions
//
useEffect(() => { useEffect(() => {
if (!appConfig.metadataCacheUri) return const cancelTokenSource = axios.CancelToken.source()
async function getTransactions() { async function transformData() {
const poolTransactions: PoolTransaction[] = []
const source = axios.CancelToken.source()
try { try {
setIsLoading(true) setIsLoading(true)
await getPoolTransactions(cancelTokenSource.token)
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()
} catch (error) { } catch (error) {
console.error('Error fetching pool transactions: ', error.message) Logger.error('Error fetching pool transactions: ', error.message)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
} }
getTransactions() transformData()
}, [accountId, chainIds, appConfig.metadataCacheUri, poolAddress, data])
return () => {
cancelTokenSource.cancel()
}
}, [getPoolTransactions])
return accountId ? ( return accountId ? (
<Table <Table
columns={minimal ? columnsMinimal : columns} columns={minimal ? columnsMinimal : columns}
data={logs} data={transactions}
isLoading={isLoading} isLoading={isLoading}
noTableHead={minimal} noTableHead={minimal}
dense={minimal} dense={minimal}
pagination={minimal ? logs?.length >= 4 : logs?.length >= 9} pagination={
minimal ? transactions?.length >= 4 : transactions?.length >= 9
}
paginationPerPage={minimal ? 5 : 10} paginationPerPage={minimal ? 5 : 10}
/> />
) : ( ) : (

View File

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

View File

@ -7,23 +7,26 @@ import jellyfish from '@oceanprotocol/art/creatures/jellyfish/jellyfish-grid.svg
import Copy from '../../../atoms/Copy' import Copy from '../../../atoms/Copy'
import Blockies from '../../../atoms/Blockies' import Blockies from '../../../atoms/Blockies'
import styles from './Account.module.css' import styles from './Account.module.css'
import { useProfile } from '../../../../providers/Profile'
export default function Account({ export default function Account({
name,
image,
accountId accountId
}: { }: {
name: string
image: string
accountId: string accountId: string
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { profile } = useProfile()
return ( return (
<div className={styles.account}> <div className={styles.account}>
<figure className={styles.imageWrap}> <figure className={styles.imageWrap}>
{image ? ( {profile?.image ? (
<img src={image} className={styles.image} width="96" height="96" /> <img
src={profile?.image}
className={styles.image}
width="96"
height="96"
/>
) : accountId ? ( ) : accountId ? (
<Blockies accountId={accountId} className={styles.image} /> <Blockies accountId={accountId} className={styles.image} />
) : ( ) : (
@ -37,7 +40,9 @@ export default function Account({
</figure> </figure>
<div> <div>
<h3 className={styles.name}>{name || accountTruncate(accountId)}</h3> <h3 className={styles.name}>
{profile?.name || accountTruncate(accountId)}
</h3>
{accountId && ( {accountId && (
<code className={styles.accountId}> <code className={styles.accountId}>
{accountId} <Copy text={accountId} /> {accountId} <Copy text={accountId} />

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { DDO, Logger } from '@oceanprotocol/lib' import { Logger } from '@oceanprotocol/lib'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ReactElement } from 'react-markdown' import { ReactElement } from 'react-markdown'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../providers/UserPreferences'
@ -6,16 +6,27 @@ import {
getAccountLiquidityInOwnAssets, getAccountLiquidityInOwnAssets,
getAccountNumberOfOrders, getAccountNumberOfOrders,
getAssetsBestPrices, getAssetsBestPrices,
UserTVL UserLiquidity,
calculateUserLiquidity
} from '../../../../utils/subgraph' } from '../../../../utils/subgraph'
import Conversion from '../../../atoms/Price/Conversion' import Conversion from '../../../atoms/Price/Conversion'
import NumberUnit from '../../../molecules/NumberUnit' import NumberUnit from '../../../molecules/NumberUnit'
import styles from './Stats.module.css' import styles from './Stats.module.css'
import { import { useProfile } from '../../../../providers/Profile'
queryMetadata, import { PoolShares_poolShares as PoolShare } from '../../../../@types/apollo/PoolShares'
transformChainIdsListToQuery
} from '../../../../utils/aquarius' async function getPoolSharesLiquidity(
import axios from 'axios' poolShares: PoolShare[]
): Promise<number> {
let totalLiquidity = 0
for (const poolShare of poolShares) {
const poolLiquidity = calculateUserLiquidity(poolShare)
totalLiquidity += poolLiquidity
}
return totalLiquidity
}
export default function Stats({ export default function Stats({
accountId accountId
@ -23,80 +34,91 @@ export default function Stats({
accountId: string accountId: string
}): ReactElement { }): ReactElement {
const { chainIds } = useUserPreferences() const { chainIds } = useUserPreferences()
const { poolShares, assets, assetsTotal, downloadsTotal } = useProfile()
const [publishedAssets, setPublishedAssets] = useState<DDO[]>()
const [numberOfAssets, setNumberOfAssets] = useState(0)
const [sold, setSold] = useState(0) const [sold, setSold] = useState(0)
const [tvl, setTvl] = useState<UserTVL>() const [publisherLiquidity, setPublisherLiquidity] = useState<UserLiquidity>()
const [totalLiquidity, setTotalLiquidity] = useState(0)
useEffect(() => { useEffect(() => {
if (!accountId) { if (!accountId) {
setNumberOfAssets(0)
setSold(0) setSold(0)
setTvl({ price: '0', oceanBalance: '0' }) setPublisherLiquidity({ price: '0', oceanBalance: '0' })
setTotalLiquidity(0)
return return
} }
async function getPublished() { async function getSales() {
const queryPublishedAssets = { if (!assets) return
query: {
query_string: {
query: `(publicKey.owner:${accountId}) AND (${transformChainIdsListToQuery(
chainIds
)})`
}
}
}
try { try {
const source = axios.CancelToken.source() const nrOrders = await getAccountNumberOfOrders(assets, chainIds)
const result = await queryMetadata(queryPublishedAssets, source.token)
setPublishedAssets(result.results)
setNumberOfAssets(result.totalResults)
const nrOrders = await getAccountNumberOfOrders(
result.results,
chainIds
)
setSold(nrOrders) setSold(nrOrders)
} catch (error) { } catch (error) {
Logger.error(error.message) Logger.error(error.message)
} }
} }
getPublished() getSales()
}, [accountId, chainIds]) }, [accountId, chainIds, assets])
useEffect(() => { useEffect(() => {
if (!publishedAssets) return if (!assets || !accountId || !chainIds) return
async function getAccountTVL() { async function getPublisherLiquidity() {
try { try {
const accountPoolAdresses: string[] = [] const accountPoolAdresses: string[] = []
const assetsPrices = await getAssetsBestPrices(publishedAssets) const assetsPrices = await getAssetsBestPrices(assets)
for (const priceInfo of assetsPrices) { for (const priceInfo of assetsPrices) {
if (priceInfo.price.type === 'pool') { if (priceInfo.price.type === 'pool') {
accountPoolAdresses.push(priceInfo.price.address.toLowerCase()) accountPoolAdresses.push(priceInfo.price.address.toLowerCase())
} }
} }
const userTvl: UserTVL = await getAccountLiquidityInOwnAssets( const userLiquidity = await getAccountLiquidityInOwnAssets(
accountId, accountId,
chainIds, chainIds,
accountPoolAdresses accountPoolAdresses
) )
setTvl(userTvl) setPublisherLiquidity(userLiquidity)
} catch (error) { } catch (error) {
Logger.error(error.message) Logger.error(error.message)
} }
} }
getAccountTVL() getPublisherLiquidity()
}, [publishedAssets, accountId, chainIds]) }, [assets, accountId, chainIds])
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 ( return (
<div className={styles.stats}> <div className={styles.stats}>
<NumberUnit <NumberUnit
label="Total Value Locked" label="Liquidity in Own Assets"
value={<Conversion price={tvl?.price} hideApproximateSymbol />} value={
<Conversion price={publisherLiquidity?.price} hideApproximateSymbol />
}
/>
<NumberUnit
label="Total Liquidity"
value={<Conversion price={`${totalLiquidity}`} hideApproximateSymbol />}
/>
<NumberUnit label={`Sale${sold === 1 ? '' : 's'}`} value={sold} />
<NumberUnit label="Published" value={assetsTotal} />
<NumberUnit
label={`Download${downloadsTotal === 1 ? '' : 's'}`}
tooltip="Datatoken orders for assets with `access` service, as opposed to `compute`. As one order could allow multiple or infinite downloads this number does not reflect the actual download count of an asset file."
value={downloadsTotal}
/> />
<NumberUnit label="Sold" value={sold} />
<NumberUnit label="Published" value={numberOfAssets} />
</div> </div>
) )
} }

View File

@ -1,26 +1,16 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useState } from 'react'
import get3BoxProfile from '../../../../utils/profile'
import { Profile } from '../../../../models/Profile'
import { accountTruncate } from '../../../../utils/web3'
import axios from 'axios'
import PublisherLinks from './PublisherLinks' import PublisherLinks from './PublisherLinks'
import Markdown from '../../../atoms/Markdown' import Markdown from '../../../atoms/Markdown'
import Stats from './Stats' import Stats from './Stats'
import Account from './Account' import Account from './Account'
import styles from './index.module.css' import styles from './index.module.css'
import { useProfile } from '../../../../providers/Profile'
const isDescriptionTextClamped = () => { const isDescriptionTextClamped = () => {
const el = document.getElementById('description') const el = document.getElementById('description')
if (el) return el.scrollHeight > el.clientHeight if (el) return el.scrollHeight > el.clientHeight
} }
const clearedProfile: Profile = {
name: null,
image: null,
description: null,
links: null
}
const Link3Box = ({ accountId, text }: { accountId: string; text: string }) => { const Link3Box = ({ accountId, text }: { accountId: string; text: string }) => {
return ( return (
<a <a
@ -38,62 +28,22 @@ export default function AccountHeader({
}: { }: {
accountId: string accountId: string
}): ReactElement { }): ReactElement {
const [profile, setProfile] = useState<Profile>({ const { profile } = useProfile()
name: accountTruncate(accountId),
image: null,
description: null,
links: null
})
const [isShowMore, setIsShowMore] = useState(false) const [isShowMore, setIsShowMore] = useState(false)
const toogleShowMore = () => { const toogleShowMore = () => {
setIsShowMore(!isShowMore) setIsShowMore(!isShowMore)
} }
useEffect(() => {
if (!accountId) {
setProfile(clearedProfile)
return
}
const source = axios.CancelToken.source()
async function getInfoFrom3Box() {
const profile3Box = await get3BoxProfile(accountId, source.token)
if (profile3Box) {
const { name, emoji, description, image, links } = profile3Box
const newName = `${emoji || ''} ${name || accountTruncate(accountId)}`
const newProfile = {
name: newName,
image,
description,
links
}
setProfile(newProfile)
} else {
setProfile(clearedProfile)
}
}
getInfoFrom3Box()
return () => {
source.cancel()
}
}, [accountId])
return ( return (
<div className={styles.grid}> <div className={styles.grid}>
<div> <div>
<Account <Account accountId={accountId} />
accountId={accountId}
image={profile.image}
name={profile.name}
/>
<Stats accountId={accountId} /> <Stats accountId={accountId} />
</div> </div>
<div> <div>
<Markdown text={profile.description} className={styles.description} /> <Markdown text={profile?.description} className={styles.description} />
{isDescriptionTextClamped() ? ( {isDescriptionTextClamped() ? (
<span className={styles.more} onClick={toogleShowMore}> <span className={styles.more} onClick={toogleShowMore}>
<Link3Box accountId={accountId} text="Read more on 3box" /> <Link3Box accountId={accountId} text="Read more on 3box" />
@ -101,11 +51,8 @@ export default function AccountHeader({
) : ( ) : (
'' ''
)} )}
{profile.links?.length > 0 && ( {profile?.links?.length > 0 && (
<PublisherLinks <PublisherLinks className={styles.publisherLinks} />
links={profile.links}
className={styles.publisherLinks}
/>
)} )}
</div> </div>
<div className={styles.meta}> <div className={styles.meta}>

View File

@ -1,64 +1,33 @@
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement } from 'react'
import Table from '../../../atoms/Table' import Table from '../../../atoms/Table'
import { gql } from 'urql'
import Time from '../../../atoms/Time' import Time from '../../../atoms/Time'
import web3 from 'web3'
import AssetTitle from '../../../molecules/AssetListTitle' import AssetTitle from '../../../molecules/AssetListTitle'
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' import NetworkName from '../../../atoms/NetworkName'
import { useProfile } from '../../../../providers/Profile'
const getTokenOrders = gql` import { DownloadedAsset } from '../../../../utils/aquarius'
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 = [ const columns = [
{ {
name: 'Data Set', name: 'Data Set',
selector: function getAssetRow(row: DownloadedAssets) { selector: function getAssetRow(row: DownloadedAsset) {
return <AssetTitle did={row.did} /> return <AssetTitle ddo={row.ddo} />
} }
}, },
{ {
name: 'Network', name: 'Network',
selector: function getNetwork(row: DownloadedAssets) { selector: function getNetwork(row: DownloadedAsset) {
return <NetworkName networkId={row.networkId} /> return <NetworkName networkId={row.networkId} />
} }
}, },
{ {
name: 'Datatoken', name: 'Datatoken',
selector: function getTitleRow(row: DownloadedAssets) { selector: function getTitleRow(row: DownloadedAsset) {
return row.dtSymbol return row.dtSymbol
} }
}, },
{ {
name: 'Time', name: 'Time',
selector: function getTimeRow(row: DownloadedAssets) { selector: function getTimeRow(row: DownloadedAsset) {
return <Time date={row.timestamp.toString()} relative isUnix /> return <Time date={row.timestamp.toString()} relative isUnix />
} }
} }
@ -69,67 +38,14 @@ export default function ComputeDownloads({
}: { }: {
accountId: string accountId: string
}): ReactElement { }): ReactElement {
const { appConfig } = useSiteMetadata() const { downloads, isDownloadsLoading } = useProfile()
const [isLoading, setIsLoading] = useState(false)
const [orders, setOrders] = useState<DownloadedAssets[]>()
const { chainIds } = useUserPreferences()
useEffect(() => {
const variables = { user: accountId?.toLowerCase() }
async function filterAssets() {
const filteredOrders: DownloadedAssets[] = []
const source = axios.CancelToken.source()
try {
setIsLoading(true)
const response = await fetchDataForMultipleChains(
getTokenOrders,
variables,
chainIds
)
const data: OrdersData[] = []
for (let i = 0; i < response.length; i++) {
response[i].tokenOrders.forEach((tokenOrder: OrdersData) => {
data.push(tokenOrder)
})
}
for (let i = 0; i < data.length; i++) {
const did = web3.utils
.toChecksumAddress(data[i].datatokenId.address)
.replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, source.token)
if (!ddo) continue
if (ddo.service[1].type === 'access') {
filteredOrders.push({
did: did,
networkId: ddo.chainId,
dtSymbol: data[i].datatokenId.symbol,
timestamp: data[i].timestamp
})
}
}
const sortedOrders = filteredOrders.sort(
(a, b) => b.timestamp - a.timestamp
)
setOrders(sortedOrders)
} catch (err) {
Logger.log(err.message)
} finally {
setIsLoading(false)
}
}
filterAssets()
}, [accountId, appConfig.metadataCacheUri, chainIds])
return accountId ? ( return accountId ? (
<Table <Table
columns={columns} columns={columns}
data={orders} data={downloads}
paginationPerPage={10} paginationPerPage={10}
isLoading={isLoading} isLoading={isDownloadsLoading}
/> />
) : ( ) : (
<div>Please connect your Web3 wallet.</div> <div>Please connect your Web3 wallet.</div>

View File

@ -3,61 +3,31 @@ import Table from '../../../atoms/Table'
import Conversion from '../../../atoms/Price/Conversion' import Conversion from '../../../atoms/Price/Conversion'
import styles from './PoolShares.module.css' import styles from './PoolShares.module.css'
import AssetTitle from '../../../molecules/AssetListTitle' import AssetTitle from '../../../molecules/AssetListTitle'
import { gql } from 'urql'
import { import {
PoolShares_poolShares as PoolShare, PoolShares_poolShares as PoolShare,
PoolShares_poolShares_poolId_tokens as PoolSharePoolIdTokens PoolShares_poolShares_poolId_tokens as PoolSharePoolIdTokens
} from '../../../../@types/apollo/PoolShares' } from '../../../../@types/apollo/PoolShares'
import web3 from 'web3' import web3 from 'web3'
import Token from '../../../organisms/AssetActions/Pool/Token' import Token from '../../../organisms/AssetActions/Pool/Token'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { calculateUserLiquidity } from '../../../../utils/subgraph'
import {
fetchDataForMultipleChains,
calculateUserLiquidity
} from '../../../../utils/subgraph'
import NetworkName from '../../../atoms/NetworkName' import NetworkName from '../../../atoms/NetworkName'
import axios from 'axios' import axios, { CancelToken } from 'axios'
import { retrieveDDO } from '../../../../utils/aquarius' import { retrieveDDO } from '../../../../utils/aquarius'
import { isValidNumber } from '../../../../utils/numberValidations' import { isValidNumber } from '../../../../utils/numberValidations'
import Decimal from 'decimal.js' import Decimal from 'decimal.js'
import { useProfile } from '../../../../providers/Profile'
import { DDO } from '@oceanprotocol/lib'
Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 }) Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 })
const REFETCH_INTERVAL = 20000 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 { interface Asset {
userLiquidity: number userLiquidity: number
poolShare: PoolShare poolShare: PoolShare
networkId: number networkId: number
createTime: number createTime: number
ddo: DDO
} }
function findTokenByType(tokens: PoolSharePoolIdTokens[], type: string) { function findTokenByType(tokens: PoolSharePoolIdTokens[], type: string) {
@ -126,10 +96,7 @@ const columns = [
{ {
name: 'Data Set', name: 'Data Set',
selector: function getAssetRow(row: Asset) { selector: function getAssetRow(row: Asset) {
const did = web3.utils return <AssetTitle ddo={row.ddo} />
.toChecksumAddress(row.poolShare.poolId.datatokenAddress)
.replace('0x', 'did:op:')
return <AssetTitle did={did} />
}, },
grow: 2 grow: 2
}, },
@ -161,38 +128,27 @@ const columns = [
} }
] ]
async function getPoolSharesData(accountId: string, chainIds: number[]) { async function getPoolSharesAssets(
const variables = { user: accountId?.toLowerCase() } data: PoolShare[],
const data: PoolShare[] = [] cancelToken: CancelToken
const result = await fetchDataForMultipleChains( ) {
poolSharesQuery,
variables,
chainIds
)
for (let i = 0; i < result.length; i++) {
result[i].poolShares.forEach((poolShare: PoolShare) => {
data.push(poolShare)
})
}
return data
}
async function getPoolSharesAssets(data: PoolShare[]) {
const assetList: Asset[] = [] const assetList: Asset[] = []
const source = axios.CancelToken.source()
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const did = web3.utils const did = web3.utils
.toChecksumAddress(data[i].poolId.datatokenAddress) .toChecksumAddress(data[i].poolId.datatokenAddress)
.replace('0x', 'did:op:') .replace('0x', 'did:op:')
const ddo = await retrieveDDO(did, source.token) const ddo = await retrieveDDO(did, cancelToken)
const userLiquidity = calculateUserLiquidity(data[i]) const userLiquidity = calculateUserLiquidity(data[i])
assetList.push({
poolShare: data[i], ddo &&
userLiquidity: userLiquidity, assetList.push({
networkId: ddo?.chainId, poolShare: data[i],
createTime: data[i].poolId.createTime userLiquidity: userLiquidity,
}) networkId: ddo?.chainId,
createTime: data[i].poolId.createTime,
ddo
})
} }
const assets = assetList.sort((a, b) => b.createTime - a.createTime) const assets = assetList.sort((a, b) => b.createTime - a.createTime)
return assets return assets
@ -203,33 +159,36 @@ export default function PoolShares({
}: { }: {
accountId: string accountId: string
}): ReactElement { }): ReactElement {
const { poolShares, isPoolSharesLoading } = useProfile()
const [assets, setAssets] = useState<Asset[]>() const [assets, setAssets] = useState<Asset[]>()
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>() const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
const { chainIds } = useUserPreferences()
const fetchPoolSharesData = useCallback(async () => { const fetchPoolSharesAssets = useCallback(
try { async (cancelToken: CancelToken) => {
const data = await getPoolSharesData(accountId, chainIds) if (!poolShares || isPoolSharesLoading) return
const newAssets = await getPoolSharesAssets(data)
if (JSON.stringify(assets) !== JSON.stringify(newAssets)) { try {
setAssets(newAssets) const assets = await getPoolSharesAssets(poolShares, cancelToken)
setAssets(assets)
} catch (error) {
console.error('Error fetching pool shares: ', error.message)
} }
} catch (error) { },
console.error('Error fetching pool shares: ', error.message) [poolShares, isPoolSharesLoading]
} )
}, [accountId, assets, chainIds])
useEffect(() => { useEffect(() => {
const cancelTokenSource = axios.CancelToken.source()
async function init() { async function init() {
setLoading(true) setLoading(true)
await fetchPoolSharesData() await fetchPoolSharesAssets(cancelTokenSource.token)
setLoading(false) setLoading(false)
if (dataFetchInterval) return if (dataFetchInterval) return
const interval = setInterval(async () => { const interval = setInterval(async () => {
await fetchPoolSharesData() await fetchPoolSharesAssets(cancelTokenSource.token)
}, REFETCH_INTERVAL) }, REFETCH_INTERVAL)
setDataFetchInterval(interval) setDataFetchInterval(interval)
} }
@ -237,8 +196,9 @@ export default function PoolShares({
return () => { return () => {
clearInterval(dataFetchInterval) clearInterval(dataFetchInterval)
cancelTokenSource.cancel()
} }
}, [dataFetchInterval, fetchPoolSharesData]) }, [dataFetchInterval, fetchPoolSharesAssets])
return accountId ? ( return accountId ? (
<Table <Table

View File

@ -2,15 +2,12 @@ import { Logger } from '@oceanprotocol/lib'
import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache' import { QueryResult } from '@oceanprotocol/lib/dist/node/metadatacache/MetadataCache'
import React, { ReactElement, useEffect, useState } from 'react' import React, { ReactElement, useEffect, useState } from 'react'
import AssetList from '../../../organisms/AssetList' import AssetList from '../../../organisms/AssetList'
import axios from 'axios' import { getPublishedAssets } from '../../../../utils/aquarius'
import {
queryMetadata,
transformChainIdsListToQuery
} from '../../../../utils/aquarius'
import Filters from '../../../templates/Search/Filters' import Filters from '../../../templates/Search/Filters'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata' import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
import { useUserPreferences } from '../../../../providers/UserPreferences' import { useUserPreferences } from '../../../../providers/UserPreferences'
import styles from './PublishedList.module.css' import styles from './PublishedList.module.css'
import axios from 'axios'
export default function PublishedList({ export default function PublishedList({
accountId accountId
@ -23,30 +20,23 @@ export default function PublishedList({
const [queryResult, setQueryResult] = useState<QueryResult>() const [queryResult, setQueryResult] = useState<QueryResult>()
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [page, setPage] = useState<number>(1) const [page, setPage] = useState<number>(1)
const [service, setServiceType] = useState<string>() const [service, setServiceType] = useState('dataset OR algorithm')
useEffect(() => { useEffect(() => {
async function getPublished() { if (!accountId) return
const serviceFiter =
service === undefined ? 'dataset OR algorithm' : `${service}`
if (!accountId) return
const queryPublishedAssets = {
page: page,
offset: 9,
query: {
query_string: {
query: `(publicKey.owner:${accountId}) AND (service.attributes.main.type:${serviceFiter}) AND (${transformChainIdsListToQuery(
chainIds
)})`
}
},
sort: { created: -1 }
}
try {
const source = axios.CancelToken.source()
const cancelTokenSource = axios.CancelToken.source()
async function getPublished() {
try {
setIsLoading(true) setIsLoading(true)
const result = await queryMetadata(queryPublishedAssets, source.token) const result = await getPublishedAssets(
accountId,
chainIds,
cancelTokenSource.token,
page,
service
)
setQueryResult(result) setQueryResult(result)
} catch (error) { } catch (error) {
Logger.error(error.message) Logger.error(error.message)
@ -55,6 +45,10 @@ export default function PublishedList({
} }
} }
getPublished() getPublished()
return () => {
cancelTokenSource.cancel()
}
}, [accountId, page, appConfig.metadataCacheUri, chainIds, service]) }, [accountId, page, appConfig.metadataCacheUri, chainIds, service])
return accountId ? ( return accountId ? (
@ -75,6 +69,7 @@ export default function PublishedList({
setPage(newPage) setPage(newPage)
}} }}
className={styles.assets} className={styles.assets}
noPublisher
/> />
</> </>
) : ( ) : (

View File

@ -4,6 +4,7 @@ import { graphql, PageProps } from 'gatsby'
import ProfilePage from '../../components/pages/Profile' import ProfilePage from '../../components/pages/Profile'
import { accountTruncate } from '../../utils/web3' import { accountTruncate } from '../../utils/web3'
import { useWeb3 } from '../../providers/Web3' import { useWeb3 } from '../../providers/Web3'
import ProfileProvider from '../../providers/Profile'
export default function PageGatsbyProfile(props: PageProps): ReactElement { export default function PageGatsbyProfile(props: PageProps): ReactElement {
const { accountId } = useWeb3() const { accountId } = useWeb3()
@ -18,7 +19,9 @@ export default function PageGatsbyProfile(props: PageProps): ReactElement {
return ( return (
<Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader> <Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader>
<ProfilePage accountId={finalAccountId} /> <ProfileProvider accountId={finalAccountId}>
<ProfilePage accountId={finalAccountId} />
</ProfileProvider>
</Page> </Page>
) )
} }

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

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

View File

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

View File

@ -1,5 +1,5 @@
import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql' import { gql, OperationResult, TypedDocumentNode, OperationContext } from 'urql'
import { DDO } from '@oceanprotocol/lib' import { DDO, Logger } from '@oceanprotocol/lib'
import { getUrqlClientInstance } from '../providers/UrqlProvider' import { getUrqlClientInstance } from '../providers/UrqlProvider'
import { getOceanConfig } from './ocean' import { getOceanConfig } from './ocean'
import web3 from 'web3' import web3 from 'web3'
@ -25,8 +25,9 @@ import {
PoolShares_poolShares as PoolShare PoolShares_poolShares as PoolShare
} from '../@types/apollo/PoolShares' } from '../@types/apollo/PoolShares'
import { BestPrice } from '../models/BestPrice' import { BestPrice } from '../models/BestPrice'
import { OrdersData_tokenOrders as OrdersData } from '../@types/apollo/OrdersData'
export interface UserTVL { export interface UserLiquidity {
price: string price: string
oceanBalance: string oceanBalance: string
} }
@ -206,6 +207,52 @@ const UserSharesQuery = gql`
} }
} }
` `
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 { export function getSubgraphUri(chainId: number): string {
const config = getOceanConfig(chainId) const config = getOceanConfig(chainId)
return config.subgraphUri return config.subgraphUri
@ -604,7 +651,7 @@ export async function getAccountLiquidityInOwnAssets(
accountId: string, accountId: string,
chainIds: number[], chainIds: number[],
pools: string[] pools: string[]
): Promise<UserTVL> { ): Promise<UserLiquidity> {
const queryVariables = { const queryVariables = {
user: accountId.toLowerCase(), user: accountId.toLowerCase(),
pools: pools pools: pools
@ -630,3 +677,48 @@ export async function getAccountLiquidityInOwnAssets(
oceanBalance: totalOceanLiquidity.toString() oceanBalance: totalOceanLiquidity.toString()
} }
} }
export async function getPoolSharesData(
accountId: string,
chainIds: number[]
): Promise<PoolShare[]> {
const variables = { user: accountId?.toLowerCase() }
const data: PoolShare[] = []
const result = await fetchDataForMultipleChains(
userPoolSharesQuery,
variables,
chainIds
)
for (let i = 0; i < result.length; i++) {
result[i].poolShares.forEach((poolShare: PoolShare) => {
data.push(poolShare)
})
}
return data
}
export async function getUserTokenOrders(
accountId: string,
chainIds: number[]
): Promise<OrdersData[]> {
const data: OrdersData[] = []
const variables = { user: accountId?.toLowerCase() }
try {
const tokenOrders = await fetchDataForMultipleChains(
UserTokenOrders,
variables,
chainIds
)
for (let i = 0; i < tokenOrders?.length; i++) {
tokenOrders[i].tokenOrders.forEach((tokenOrder: OrdersData) => {
data.push(tokenOrder)
})
}
return data
} catch (error) {
Logger.error(error.message)
}
}