mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01: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:
parent
5a336bd699
commit
032606e61c
@ -9,6 +9,10 @@
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
@ -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)
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { DDO } from '@oceanprotocol/lib'
|
||||
import { useOcean } from '../../providers/Ocean'
|
||||
import { Link } from 'gatsby'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import { getAssetsNames } from '../../utils/aquarius'
|
||||
@ -43,7 +42,7 @@ export default function AssetListTitle({
|
||||
|
||||
return (
|
||||
<h3 className={styles.title}>
|
||||
<Link to={`/asset/${did || ddo.id}`}>{assetTitle}</Link>
|
||||
<Link to={`/asset/${did || ddo?.id}`}>{assetTitle}</Link>
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
@ -13,11 +13,13 @@ import { BestPrice } from '../../models/BestPrice'
|
||||
declare type AssetTeaserProps = {
|
||||
ddo: DDO
|
||||
price: BestPrice
|
||||
noPublisher?: boolean
|
||||
}
|
||||
|
||||
const AssetTeaser: React.FC<AssetTeaserProps> = ({
|
||||
ddo,
|
||||
price
|
||||
price,
|
||||
noPublisher
|
||||
}: AssetTeaserProps) => {
|
||||
const { attributes } = ddo.findServiceByType('metadata')
|
||||
const { name, type } = attributes.main
|
||||
@ -34,7 +36,9 @@ const AssetTeaser: React.FC<AssetTeaserProps> = ({
|
||||
<Dotdotdot clamp={3}>
|
||||
<h1 className={styles.title}>{name}</h1>
|
||||
</Dotdotdot>
|
||||
<Publisher account={owner} minimal className={styles.publisher} />
|
||||
{!noPublisher && (
|
||||
<Publisher account={owner} minimal className={styles.publisher} />
|
||||
)}
|
||||
</header>
|
||||
|
||||
<AssetType
|
||||
|
@ -5,10 +5,7 @@ import { Logger } from '@oceanprotocol/lib'
|
||||
import Price from '../atoms/Price'
|
||||
import Tooltip from '../atoms/Tooltip'
|
||||
import AssetTitle from './AssetListTitle'
|
||||
import {
|
||||
queryMetadata,
|
||||
transformChainIdsListToQuery
|
||||
} from '../../utils/aquarius'
|
||||
import { getAssetsFromDidList } from '../../utils/aquarius'
|
||||
import { getAssetsBestPrices, AssetListPrices } from '../../utils/subgraph'
|
||||
import axios, { CancelToken } from 'axios'
|
||||
import { useSiteMetadata } from '../../hooks/useSiteMetadata'
|
||||
@ -18,31 +15,8 @@ async function getAssetsBookmarked(
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
) {
|
||||
const searchDids = JSON.stringify(bookmarks)
|
||||
.replace(/,/g, ' ')
|
||||
.replace(/"/g, '')
|
||||
.replace(/(\[|\])/g, '')
|
||||
// for whatever reason ddo.id is not searchable, so use ddo.dataToken instead
|
||||
.replace(/(did:op:)/g, '0x')
|
||||
|
||||
const queryBookmarks = {
|
||||
page: 1,
|
||||
offset: 100,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `(${searchDids}) AND (${transformChainIdsListToQuery(
|
||||
chainIds
|
||||
)})`,
|
||||
fields: ['dataToken'],
|
||||
default_operator: 'OR'
|
||||
}
|
||||
},
|
||||
sort: { created: -1 }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await queryMetadata(queryBookmarks, cancelToken)
|
||||
|
||||
const result = await getAssetsFromDidList(bookmarks, chainIds, cancelToken)
|
||||
return result
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
@ -88,7 +62,7 @@ export default function Bookmarks(): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
|
||||
useEffect(() => {
|
||||
if (!appConfig.metadataCacheUri || bookmarks === []) return
|
||||
if (!appConfig?.metadataCacheUri || bookmarks === []) return
|
||||
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
@ -121,7 +95,7 @@ export default function Bookmarks(): ReactElement {
|
||||
return () => {
|
||||
source.cancel()
|
||||
}
|
||||
}, [bookmarks, chainIds])
|
||||
}, [bookmarks, chainIds, appConfig?.metadataCacheUri])
|
||||
|
||||
return (
|
||||
<Table
|
||||
|
@ -3,6 +3,7 @@
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-h4);
|
||||
color: var(--font-color-heading);
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.number {
|
||||
@ -46,3 +47,9 @@
|
||||
background: var(--brand-white);
|
||||
border: 0.1rem solid var(--brand-pink);
|
||||
}
|
||||
|
||||
.tooltip svg {
|
||||
width: 0.8em !important;
|
||||
height: 0.8em !important;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
@ -1,45 +1,38 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import Markdown from '../atoms/Markdown'
|
||||
import Tooltip from '../atoms/Tooltip'
|
||||
import styles from './NumberUnit.module.css'
|
||||
|
||||
interface NumberInnerProps {
|
||||
interface NumberUnitProps {
|
||||
label: string
|
||||
value: number | string | Element | ReactElement
|
||||
small?: boolean
|
||||
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({
|
||||
link,
|
||||
linkTooltip,
|
||||
small,
|
||||
label,
|
||||
value,
|
||||
icon
|
||||
icon,
|
||||
tooltip
|
||||
}: NumberUnitProps): ReactElement {
|
||||
return (
|
||||
<div className={styles.unit}>
|
||||
{link ? (
|
||||
<a href={link} title={linkTooltip}>
|
||||
<NumberInner small={small} label={label} value={value} icon={icon} />
|
||||
</a>
|
||||
) : (
|
||||
<NumberInner small={small} label={label} value={value} icon={icon} />
|
||||
)}
|
||||
<div className={`${styles.number} ${small && styles.small}`}>
|
||||
{icon && icon}
|
||||
{value}
|
||||
</div>
|
||||
<span className={styles.label}>
|
||||
{label}{' '}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
content={<Markdown text={tooltip} />}
|
||||
className={styles.tooltip}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -68,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)
|
||||
|
@ -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'
|
||||
@ -10,9 +10,10 @@ 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
|
||||
|
||||
@ -75,6 +76,7 @@ export interface Datatoken {
|
||||
|
||||
export interface PoolTransaction extends TransactionHistoryPoolTransactions {
|
||||
networkId: number
|
||||
ddo: DDO
|
||||
}
|
||||
|
||||
const columns = [
|
||||
@ -87,11 +89,7 @@ const columns = [
|
||||
{
|
||||
name: 'Data Set',
|
||||
selector: function getAssetRow(row: PoolTransaction) {
|
||||
const did = web3.utils
|
||||
.toChecksumAddress(row.poolAddress.datatokenAddress)
|
||||
.replace('0x', 'did:op:')
|
||||
|
||||
return <AssetTitle did={did} />
|
||||
return <AssetTitle ddo={row.ddo} />
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -131,14 +129,14 @@ export default function PoolTransactions({
|
||||
minimal?: boolean
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const [logs, setLogs] = useState<PoolTransaction[]>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const [transactions, setTransactions] = useState<PoolTransaction[]>()
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
const { chainIds } = useUserPreferences()
|
||||
const { appConfig } = useSiteMetadata()
|
||||
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
|
||||
const [data, setData] = useState<PoolTransaction[]>()
|
||||
|
||||
async function fetchPoolTransactionData() {
|
||||
const getPoolTransactionData = useCallback(async () => {
|
||||
const variables = {
|
||||
user: accountId?.toLowerCase(),
|
||||
pool: poolAddress?.toLowerCase()
|
||||
@ -159,71 +157,94 @@ export default function PoolTransactions({
|
||||
if (JSON.stringify(data) !== JSON.stringify(transactions)) {
|
||||
setData(transactions)
|
||||
}
|
||||
}
|
||||
}, [accountId, chainIds, data, poolAddress, poolChainId])
|
||||
|
||||
function refetchPoolTransactions() {
|
||||
if (!dataFetchInterval) {
|
||||
setDataFetchInterval(
|
||||
setInterval(function () {
|
||||
fetchPoolTransactionData()
|
||||
}, REFETCH_INTERVAL)
|
||||
const getPoolTransactions = useCallback(
|
||||
async (cancelToken: CancelToken) => {
|
||||
if (!data) return
|
||||
|
||||
const poolTransactions: PoolTransaction[] = []
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const { datatokenAddress } = data[i].poolAddress
|
||||
const did = web3.utils
|
||||
.toChecksumAddress(datatokenAddress)
|
||||
.replace('0x', 'did:op:')
|
||||
const ddo = await retrieveDDO(did, cancelToken)
|
||||
ddo &&
|
||||
poolTransactions.push({
|
||||
...data[i],
|
||||
networkId: ddo?.chainId,
|
||||
ddo
|
||||
})
|
||||
}
|
||||
const sortedTransactions = poolTransactions.sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
setTransactions(sortedTransactions)
|
||||
},
|
||||
[data]
|
||||
)
|
||||
|
||||
//
|
||||
// Get data, periodically
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!appConfig?.metadataCacheUri) return
|
||||
|
||||
async function getTransactions() {
|
||||
try {
|
||||
await getPoolTransactionData()
|
||||
|
||||
if (dataFetchInterval) return
|
||||
const interval = setInterval(async () => {
|
||||
await getPoolTransactionData()
|
||||
}, REFETCH_INTERVAL)
|
||||
setDataFetchInterval(interval)
|
||||
} catch (error) {
|
||||
Logger.error('Error fetching pool transactions: ', error.message)
|
||||
}
|
||||
}
|
||||
getTransactions()
|
||||
|
||||
return () => {
|
||||
clearInterval(dataFetchInterval)
|
||||
}
|
||||
}, [dataFetchInterval])
|
||||
}, [getPoolTransactionData, dataFetchInterval, appConfig.metadataCacheUri])
|
||||
|
||||
//
|
||||
// Transform to final transactions
|
||||
//
|
||||
useEffect(() => {
|
||||
if (!appConfig.metadataCacheUri) return
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
async function getTransactions() {
|
||||
const poolTransactions: PoolTransaction[] = []
|
||||
const source = axios.CancelToken.source()
|
||||
async function transformData() {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
if (!data) {
|
||||
await fetchPoolTransactionData()
|
||||
return
|
||||
}
|
||||
const poolTransactionsData = data.map((obj) => ({ ...obj }))
|
||||
|
||||
for (let i = 0; i < poolTransactionsData.length; i++) {
|
||||
const did = web3.utils
|
||||
.toChecksumAddress(
|
||||
poolTransactionsData[i].poolAddress.datatokenAddress
|
||||
)
|
||||
.replace('0x', 'did:op:')
|
||||
const ddo = await retrieveDDO(did, source.token)
|
||||
poolTransactionsData[i].networkId = ddo.chainId
|
||||
poolTransactions.push(poolTransactionsData[i])
|
||||
}
|
||||
const sortedTransactions = poolTransactions.sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
)
|
||||
setLogs(sortedTransactions)
|
||||
refetchPoolTransactions()
|
||||
await getPoolTransactions(cancelTokenSource.token)
|
||||
} catch (error) {
|
||||
console.error('Error fetching pool transactions: ', error.message)
|
||||
Logger.error('Error fetching pool transactions: ', error.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
getTransactions()
|
||||
}, [accountId, chainIds, appConfig.metadataCacheUri, poolAddress, data])
|
||||
transformData()
|
||||
|
||||
return () => {
|
||||
cancelTokenSource.cancel()
|
||||
}
|
||||
}, [getPoolTransactions])
|
||||
|
||||
return accountId ? (
|
||||
<Table
|
||||
columns={minimal ? columnsMinimal : columns}
|
||||
data={logs}
|
||||
data={transactions}
|
||||
isLoading={isLoading}
|
||||
noTableHead={minimal}
|
||||
dense={minimal}
|
||||
pagination={minimal ? logs?.length >= 4 : logs?.length >= 9}
|
||||
pagination={
|
||||
minimal ? transactions?.length >= 4 : transactions?.length >= 9
|
||||
}
|
||||
paginationPerPage={minimal ? 5 : 10}
|
||||
/>
|
||||
) : (
|
||||
|
@ -26,6 +26,7 @@ declare type AssetListProps = {
|
||||
isLoading?: boolean
|
||||
onPageChange?: React.Dispatch<React.SetStateAction<number>>
|
||||
className?: string
|
||||
noPublisher?: boolean
|
||||
}
|
||||
|
||||
const AssetList: React.FC<AssetListProps> = ({
|
||||
@ -35,7 +36,8 @@ const AssetList: React.FC<AssetListProps> = ({
|
||||
totalPages,
|
||||
isLoading,
|
||||
onPageChange,
|
||||
className
|
||||
className,
|
||||
noPublisher
|
||||
}) => {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const [assetsWithPrices, setAssetWithPrices] = useState<AssetListPrices[]>()
|
||||
@ -71,6 +73,7 @@ const AssetList: React.FC<AssetListProps> = ({
|
||||
ddo={assetWithPrice.ddo}
|
||||
price={assetWithPrice.price}
|
||||
key={assetWithPrice.ddo.id}
|
||||
noPublisher={noPublisher}
|
||||
/>
|
||||
))
|
||||
) : chainIds.length === 0 ? (
|
||||
|
@ -7,23 +7,26 @@ 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({
|
||||
name,
|
||||
image,
|
||||
accountId
|
||||
}: {
|
||||
name: string
|
||||
image: string
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const { chainIds } = useUserPreferences()
|
||||
const { profile } = useProfile()
|
||||
|
||||
return (
|
||||
<div className={styles.account}>
|
||||
<figure className={styles.imageWrap}>
|
||||
{image ? (
|
||||
<img src={image} className={styles.image} width="96" height="96" />
|
||||
{profile?.image ? (
|
||||
<img
|
||||
src={profile?.image}
|
||||
className={styles.image}
|
||||
width="96"
|
||||
height="96"
|
||||
/>
|
||||
) : accountId ? (
|
||||
<Blockies accountId={accountId} className={styles.image} />
|
||||
) : (
|
||||
@ -37,7 +40,9 @@ export default function Account({
|
||||
</figure>
|
||||
|
||||
<div>
|
||||
<h3 className={styles.name}>{name || accountTruncate(accountId)}</h3>
|
||||
<h3 className={styles.name}>
|
||||
{profile?.name || accountTruncate(accountId)}
|
||||
</h3>
|
||||
{accountId && (
|
||||
<code className={styles.accountId}>
|
||||
{accountId} <Copy text={accountId} />
|
||||
|
@ -1,18 +1,18 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import classNames from 'classnames/bind'
|
||||
import { ProfileLink } from '../../../../models/Profile'
|
||||
import { ReactComponent as External } from '../../../../images/external.svg'
|
||||
import styles from './PublisherLinks.module.css'
|
||||
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
|
||||
@ -21,7 +21,7 @@ export default function PublisherLinks({
|
||||
return (
|
||||
<div className={styleClasses}>
|
||||
{' — '}
|
||||
{links?.map((link: ProfileLink) => {
|
||||
{profile?.links?.map((link) => {
|
||||
const href =
|
||||
link.name === 'Twitter'
|
||||
? `https://twitter.com/${link.value}`
|
||||
|
@ -1,6 +1,6 @@
|
||||
.stats {
|
||||
display: grid;
|
||||
gap: var(--spacer);
|
||||
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||
margin-top: calc(var(--spacer) / 2);
|
||||
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
|
||||
margin-top: var(--spacer);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DDO, Logger } from '@oceanprotocol/lib'
|
||||
import { Logger } from '@oceanprotocol/lib'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ReactElement } from 'react-markdown'
|
||||
import { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
@ -6,16 +6,27 @@ import {
|
||||
getAccountLiquidityInOwnAssets,
|
||||
getAccountNumberOfOrders,
|
||||
getAssetsBestPrices,
|
||||
UserTVL
|
||||
UserLiquidity,
|
||||
calculateUserLiquidity
|
||||
} from '../../../../utils/subgraph'
|
||||
import Conversion from '../../../atoms/Price/Conversion'
|
||||
import NumberUnit from '../../../molecules/NumberUnit'
|
||||
import styles from './Stats.module.css'
|
||||
import {
|
||||
queryMetadata,
|
||||
transformChainIdsListToQuery
|
||||
} from '../../../../utils/aquarius'
|
||||
import axios from 'axios'
|
||||
import { useProfile } from '../../../../providers/Profile'
|
||||
import { PoolShares_poolShares as PoolShare } from '../../../../@types/apollo/PoolShares'
|
||||
|
||||
async function getPoolSharesLiquidity(
|
||||
poolShares: PoolShare[]
|
||||
): Promise<number> {
|
||||
let totalLiquidity = 0
|
||||
|
||||
for (const poolShare of poolShares) {
|
||||
const poolLiquidity = calculateUserLiquidity(poolShare)
|
||||
totalLiquidity += poolLiquidity
|
||||
}
|
||||
|
||||
return totalLiquidity
|
||||
}
|
||||
|
||||
export default function Stats({
|
||||
accountId
|
||||
@ -23,80 +34,91 @@ export default function Stats({
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
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 [tvl, setTvl] = useState<UserTVL>()
|
||||
const [publisherLiquidity, setPublisherLiquidity] = useState<UserLiquidity>()
|
||||
const [totalLiquidity, setTotalLiquidity] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountId) {
|
||||
setNumberOfAssets(0)
|
||||
setSold(0)
|
||||
setTvl({ price: '0', oceanBalance: '0' })
|
||||
setPublisherLiquidity({ price: '0', oceanBalance: '0' })
|
||||
setTotalLiquidity(0)
|
||||
return
|
||||
}
|
||||
|
||||
async function getPublished() {
|
||||
const queryPublishedAssets = {
|
||||
query: {
|
||||
query_string: {
|
||||
query: `(publicKey.owner:${accountId}) AND (${transformChainIdsListToQuery(
|
||||
chainIds
|
||||
)})`
|
||||
}
|
||||
}
|
||||
}
|
||||
async function getSales() {
|
||||
if (!assets) return
|
||||
|
||||
try {
|
||||
const source = axios.CancelToken.source()
|
||||
const result = await queryMetadata(queryPublishedAssets, source.token)
|
||||
setPublishedAssets(result.results)
|
||||
setNumberOfAssets(result.totalResults)
|
||||
const nrOrders = await getAccountNumberOfOrders(
|
||||
result.results,
|
||||
chainIds
|
||||
)
|
||||
const nrOrders = await getAccountNumberOfOrders(assets, chainIds)
|
||||
setSold(nrOrders)
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
getPublished()
|
||||
}, [accountId, chainIds])
|
||||
getSales()
|
||||
}, [accountId, chainIds, assets])
|
||||
|
||||
useEffect(() => {
|
||||
if (!publishedAssets) return
|
||||
if (!assets || !accountId || !chainIds) return
|
||||
|
||||
async function getAccountTVL() {
|
||||
async function getPublisherLiquidity() {
|
||||
try {
|
||||
const accountPoolAdresses: string[] = []
|
||||
const assetsPrices = await getAssetsBestPrices(publishedAssets)
|
||||
const assetsPrices = await getAssetsBestPrices(assets)
|
||||
for (const priceInfo of assetsPrices) {
|
||||
if (priceInfo.price.type === 'pool') {
|
||||
accountPoolAdresses.push(priceInfo.price.address.toLowerCase())
|
||||
}
|
||||
}
|
||||
const userTvl: UserTVL = await getAccountLiquidityInOwnAssets(
|
||||
const userLiquidity = await getAccountLiquidityInOwnAssets(
|
||||
accountId,
|
||||
chainIds,
|
||||
accountPoolAdresses
|
||||
)
|
||||
setTvl(userTvl)
|
||||
setPublisherLiquidity(userLiquidity)
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
getAccountTVL()
|
||||
}, [publishedAssets, accountId, chainIds])
|
||||
getPublisherLiquidity()
|
||||
}, [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 (
|
||||
<div className={styles.stats}>
|
||||
<NumberUnit
|
||||
label="Total Value Locked"
|
||||
value={<Conversion price={tvl?.price} hideApproximateSymbol />}
|
||||
label="Liquidity in Own Assets"
|
||||
value={
|
||||
<Conversion price={publisherLiquidity?.price} hideApproximateSymbol />
|
||||
}
|
||||
/>
|
||||
<NumberUnit
|
||||
label="Total Liquidity"
|
||||
value={<Conversion price={`${totalLiquidity}`} hideApproximateSymbol />}
|
||||
/>
|
||||
<NumberUnit label={`Sale${sold === 1 ? '' : 's'}`} value={sold} />
|
||||
<NumberUnit label="Published" value={assetsTotal} />
|
||||
<NumberUnit
|
||||
label={`Download${downloadsTotal === 1 ? '' : 's'}`}
|
||||
tooltip="Datatoken orders for assets with `access` service, as opposed to `compute`. As one order could allow multiple or infinite downloads this number does not reflect the actual download count of an asset file."
|
||||
value={downloadsTotal}
|
||||
/>
|
||||
<NumberUnit label="Sold" value={sold} />
|
||||
<NumberUnit label="Published" value={numberOfAssets} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,26 +1,16 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import get3BoxProfile from '../../../../utils/profile'
|
||||
import { Profile } from '../../../../models/Profile'
|
||||
import { accountTruncate } from '../../../../utils/web3'
|
||||
import axios from 'axios'
|
||||
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 clearedProfile: Profile = {
|
||||
name: null,
|
||||
image: null,
|
||||
description: null,
|
||||
links: null
|
||||
}
|
||||
|
||||
const Link3Box = ({ accountId, text }: { accountId: string; text: string }) => {
|
||||
return (
|
||||
<a
|
||||
@ -38,62 +28,22 @@ export default function AccountHeader({
|
||||
}: {
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const [profile, setProfile] = useState<Profile>({
|
||||
name: accountTruncate(accountId),
|
||||
image: null,
|
||||
description: null,
|
||||
links: null
|
||||
})
|
||||
const { profile } = useProfile()
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
|
||||
const toogleShowMore = () => {
|
||||
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 (
|
||||
<div className={styles.grid}>
|
||||
<div>
|
||||
<Account
|
||||
accountId={accountId}
|
||||
image={profile.image}
|
||||
name={profile.name}
|
||||
/>
|
||||
<Account accountId={accountId} />
|
||||
<Stats accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Markdown text={profile.description} className={styles.description} />
|
||||
<Markdown text={profile?.description} className={styles.description} />
|
||||
{isDescriptionTextClamped() ? (
|
||||
<span className={styles.more} onClick={toogleShowMore}>
|
||||
<Link3Box accountId={accountId} text="Read more on 3box" />
|
||||
@ -101,11 +51,8 @@ export default function AccountHeader({
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{profile.links?.length > 0 && (
|
||||
<PublisherLinks
|
||||
links={profile.links}
|
||||
className={styles.publisherLinks}
|
||||
/>
|
||||
{profile?.links?.length > 0 && (
|
||||
<PublisherLinks className={styles.publisherLinks} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.meta}>
|
||||
|
@ -1,64 +1,33 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import React, { ReactElement } 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 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
|
||||
}
|
||||
import { useProfile } from '../../../../providers/Profile'
|
||||
import { DownloadedAsset } from '../../../../utils/aquarius'
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'Data Set',
|
||||
selector: function getAssetRow(row: DownloadedAssets) {
|
||||
return <AssetTitle did={row.did} />
|
||||
selector: function getAssetRow(row: DownloadedAsset) {
|
||||
return <AssetTitle ddo={row.ddo} />
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Network',
|
||||
selector: function getNetwork(row: DownloadedAssets) {
|
||||
selector: function getNetwork(row: DownloadedAsset) {
|
||||
return <NetworkName networkId={row.networkId} />
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Datatoken',
|
||||
selector: function getTitleRow(row: DownloadedAssets) {
|
||||
selector: function getTitleRow(row: DownloadedAsset) {
|
||||
return row.dtSymbol
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Time',
|
||||
selector: function getTimeRow(row: DownloadedAssets) {
|
||||
selector: function getTimeRow(row: DownloadedAsset) {
|
||||
return <Time date={row.timestamp.toString()} relative isUnix />
|
||||
}
|
||||
}
|
||||
@ -69,67 +38,14 @@ export default function ComputeDownloads({
|
||||
}: {
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const { appConfig } = useSiteMetadata()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [orders, setOrders] = useState<DownloadedAssets[]>()
|
||||
const { chainIds } = useUserPreferences()
|
||||
|
||||
useEffect(() => {
|
||||
const variables = { user: accountId?.toLowerCase() }
|
||||
|
||||
async function filterAssets() {
|
||||
const filteredOrders: DownloadedAssets[] = []
|
||||
const source = axios.CancelToken.source()
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await fetchDataForMultipleChains(
|
||||
getTokenOrders,
|
||||
variables,
|
||||
chainIds
|
||||
)
|
||||
|
||||
const data: OrdersData[] = []
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
response[i].tokenOrders.forEach((tokenOrder: OrdersData) => {
|
||||
data.push(tokenOrder)
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const did = web3.utils
|
||||
.toChecksumAddress(data[i].datatokenId.address)
|
||||
.replace('0x', 'did:op:')
|
||||
const ddo = await retrieveDDO(did, source.token)
|
||||
if (!ddo) continue
|
||||
if (ddo.service[1].type === 'access') {
|
||||
filteredOrders.push({
|
||||
did: did,
|
||||
networkId: ddo.chainId,
|
||||
dtSymbol: data[i].datatokenId.symbol,
|
||||
timestamp: data[i].timestamp
|
||||
})
|
||||
}
|
||||
}
|
||||
const sortedOrders = filteredOrders.sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
)
|
||||
setOrders(sortedOrders)
|
||||
} catch (err) {
|
||||
Logger.log(err.message)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
filterAssets()
|
||||
}, [accountId, appConfig.metadataCacheUri, chainIds])
|
||||
const { downloads, isDownloadsLoading } = useProfile()
|
||||
|
||||
return accountId ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={orders}
|
||||
data={downloads}
|
||||
paginationPerPage={10}
|
||||
isLoading={isLoading}
|
||||
isLoading={isDownloadsLoading}
|
||||
/>
|
||||
) : (
|
||||
<div>Please connect your Web3 wallet.</div>
|
||||
|
@ -3,61 +3,31 @@ 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_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 { useUserPreferences } from '../../../../providers/UserPreferences'
|
||||
import {
|
||||
fetchDataForMultipleChains,
|
||||
calculateUserLiquidity
|
||||
} from '../../../../utils/subgraph'
|
||||
import { calculateUserLiquidity } from '../../../../utils/subgraph'
|
||||
import NetworkName from '../../../atoms/NetworkName'
|
||||
import axios from 'axios'
|
||||
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
|
||||
|
||||
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
|
||||
ddo: DDO
|
||||
}
|
||||
|
||||
function findTokenByType(tokens: PoolSharePoolIdTokens[], type: string) {
|
||||
@ -126,10 +96,7 @@ const columns = [
|
||||
{
|
||||
name: 'Data Set',
|
||||
selector: function getAssetRow(row: Asset) {
|
||||
const did = web3.utils
|
||||
.toChecksumAddress(row.poolShare.poolId.datatokenAddress)
|
||||
.replace('0x', 'did:op:')
|
||||
return <AssetTitle did={did} />
|
||||
return <AssetTitle ddo={row.ddo} />
|
||||
},
|
||||
grow: 2
|
||||
},
|
||||
@ -161,38 +128,27 @@ const columns = [
|
||||
}
|
||||
]
|
||||
|
||||
async function getPoolSharesData(accountId: string, chainIds: number[]) {
|
||||
const variables = { user: accountId?.toLowerCase() }
|
||||
const data: PoolShare[] = []
|
||||
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[]) {
|
||||
async function getPoolSharesAssets(
|
||||
data: PoolShare[],
|
||||
cancelToken: CancelToken
|
||||
) {
|
||||
const assetList: Asset[] = []
|
||||
const source = axios.CancelToken.source()
|
||||
|
||||
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 ddo = await retrieveDDO(did, cancelToken)
|
||||
const userLiquidity = calculateUserLiquidity(data[i])
|
||||
assetList.push({
|
||||
poolShare: data[i],
|
||||
userLiquidity: userLiquidity,
|
||||
networkId: ddo?.chainId,
|
||||
createTime: data[i].poolId.createTime
|
||||
})
|
||||
|
||||
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
|
||||
@ -203,33 +159,36 @@ export default function PoolShares({
|
||||
}: {
|
||||
accountId: string
|
||||
}): ReactElement {
|
||||
const { poolShares, isPoolSharesLoading } = useProfile()
|
||||
const [assets, setAssets] = useState<Asset[]>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [dataFetchInterval, setDataFetchInterval] = useState<NodeJS.Timeout>()
|
||||
const { chainIds } = useUserPreferences()
|
||||
|
||||
const fetchPoolSharesData = useCallback(async () => {
|
||||
try {
|
||||
const data = await getPoolSharesData(accountId, chainIds)
|
||||
const newAssets = await getPoolSharesAssets(data)
|
||||
const fetchPoolSharesAssets = useCallback(
|
||||
async (cancelToken: CancelToken) => {
|
||||
if (!poolShares || isPoolSharesLoading) return
|
||||
|
||||
if (JSON.stringify(assets) !== JSON.stringify(newAssets)) {
|
||||
setAssets(newAssets)
|
||||
try {
|
||||
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)
|
||||
}
|
||||
}, [accountId, assets, chainIds])
|
||||
},
|
||||
[poolShares, isPoolSharesLoading]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
async function init() {
|
||||
setLoading(true)
|
||||
await fetchPoolSharesData()
|
||||
await fetchPoolSharesAssets(cancelTokenSource.token)
|
||||
setLoading(false)
|
||||
|
||||
if (dataFetchInterval) return
|
||||
const interval = setInterval(async () => {
|
||||
await fetchPoolSharesData()
|
||||
await fetchPoolSharesAssets(cancelTokenSource.token)
|
||||
}, REFETCH_INTERVAL)
|
||||
setDataFetchInterval(interval)
|
||||
}
|
||||
@ -237,8 +196,9 @@ export default function PoolShares({
|
||||
|
||||
return () => {
|
||||
clearInterval(dataFetchInterval)
|
||||
cancelTokenSource.cancel()
|
||||
}
|
||||
}, [dataFetchInterval, fetchPoolSharesData])
|
||||
}, [dataFetchInterval, fetchPoolSharesAssets])
|
||||
|
||||
return accountId ? (
|
||||
<Table
|
||||
|
@ -2,15 +2,12 @@ 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 { 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
|
||||
@ -23,30 +20,23 @@ export default function PublishedList({
|
||||
const [queryResult, setQueryResult] = useState<QueryResult>()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [service, setServiceType] = useState<string>()
|
||||
const [service, setServiceType] = useState('dataset OR algorithm')
|
||||
|
||||
useEffect(() => {
|
||||
async function getPublished() {
|
||||
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()
|
||||
if (!accountId) return
|
||||
|
||||
const cancelTokenSource = axios.CancelToken.source()
|
||||
|
||||
async function getPublished() {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const result = await queryMetadata(queryPublishedAssets, source.token)
|
||||
const result = await getPublishedAssets(
|
||||
accountId,
|
||||
chainIds,
|
||||
cancelTokenSource.token,
|
||||
page,
|
||||
service
|
||||
)
|
||||
setQueryResult(result)
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
@ -55,6 +45,10 @@ export default function PublishedList({
|
||||
}
|
||||
}
|
||||
getPublished()
|
||||
|
||||
return () => {
|
||||
cancelTokenSource.cancel()
|
||||
}
|
||||
}, [accountId, page, appConfig.metadataCacheUri, chainIds, service])
|
||||
|
||||
return accountId ? (
|
||||
@ -75,6 +69,7 @@ export default function PublishedList({
|
||||
setPage(newPage)
|
||||
}}
|
||||
className={styles.assets}
|
||||
noPublisher
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
@ -4,6 +4,7 @@ import { graphql, PageProps } from 'gatsby'
|
||||
import ProfilePage from '../../components/pages/Profile'
|
||||
import { accountTruncate } from '../../utils/web3'
|
||||
import { useWeb3 } from '../../providers/Web3'
|
||||
import ProfileProvider from '../../providers/Profile'
|
||||
|
||||
export default function PageGatsbyProfile(props: PageProps): ReactElement {
|
||||
const { accountId } = useWeb3()
|
||||
@ -18,7 +19,9 @@ export default function PageGatsbyProfile(props: PageProps): ReactElement {
|
||||
|
||||
return (
|
||||
<Page uri={props.uri} title={accountTruncate(finalAccountId)} noPageHeader>
|
||||
<ProfilePage accountId={finalAccountId} />
|
||||
<ProfileProvider accountId={finalAccountId}>
|
||||
<ProfilePage accountId={finalAccountId} />
|
||||
</ProfileProvider>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
291
src/providers/Profile.tsx
Normal file
291
src/providers/Profile.tsx
Normal 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
|
@ -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
|
||||
}
|
||||
|
||||
function getQueryForAlgorithmDatasets(algorithmDid: string, chainId?: number) {
|
||||
return {
|
||||
@ -66,7 +75,8 @@ export async function queryMetadata(
|
||||
try {
|
||||
const response: AxiosResponse<any> = await axios.post(
|
||||
`${metadataCacheUri}/api/v1/aquarius/assets/ddo/query`,
|
||||
{ ...query, cancelToken }
|
||||
{ ...query },
|
||||
{ cancelToken }
|
||||
)
|
||||
if (!response || response.status !== 200 || !response.data) return
|
||||
return transformQueryResult(response.data)
|
||||
@ -108,10 +118,8 @@ export async function getAssetsNames(
|
||||
try {
|
||||
const response: AxiosResponse<Record<string, string>> = await axios.post(
|
||||
`${metadataCacheUri}/api/v1/aquarius/assets/names`,
|
||||
{
|
||||
didList,
|
||||
cancelToken
|
||||
}
|
||||
{ didList },
|
||||
{ cancelToken }
|
||||
)
|
||||
if (!response || response.status !== 200 || !response.data) return
|
||||
return response.data
|
||||
@ -204,3 +212,122 @@ export async function getAlgorithmDatasetsForCompute(
|
||||
)
|
||||
return datasets
|
||||
}
|
||||
|
||||
export async function getPublishedAssets(
|
||||
accountId: string,
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken,
|
||||
page?: number,
|
||||
type?: string
|
||||
): Promise<QueryResult> {
|
||||
if (!accountId) return
|
||||
|
||||
page = page || 1
|
||||
type = type || 'dataset OR algorithm'
|
||||
|
||||
const queryPublishedAssets = {
|
||||
page,
|
||||
offset: 9,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `(publicKey.owner:${accountId}) AND (service.attributes.main.type:${type}) AND (${transformChainIdsListToQuery(
|
||||
chainIds
|
||||
)})`
|
||||
}
|
||||
},
|
||||
sort: { created: -1 }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await queryMetadata(queryPublishedAssets, cancelToken)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
Logger.log(error.message)
|
||||
} else {
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAssetsFromDidList(
|
||||
didList: string[],
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
): Promise<QueryResult> {
|
||||
try {
|
||||
// TODO: figure out cleaner way to transform string[] into csv
|
||||
const searchDids = JSON.stringify(didList)
|
||||
.replace(/,/g, ' ')
|
||||
.replace(/"/g, '')
|
||||
.replace(/(\[|\])/g, '')
|
||||
// for whatever reason ddo.id is not searchable, so use ddo.dataToken instead
|
||||
.replace(/(did:op:)/g, '0x')
|
||||
|
||||
// safeguard against passed empty didList, preventing 500 from Aquarius
|
||||
if (!searchDids) return
|
||||
|
||||
const query = {
|
||||
page: 1,
|
||||
offset: 1000,
|
||||
query: {
|
||||
query_string: {
|
||||
query: `(${searchDids}) AND (${transformChainIdsListToQuery(
|
||||
chainIds
|
||||
)})`,
|
||||
fields: ['dataToken'],
|
||||
default_operator: 'OR'
|
||||
}
|
||||
},
|
||||
sort: { created: -1 }
|
||||
}
|
||||
|
||||
const queryResult = await queryMetadata(query, cancelToken)
|
||||
return queryResult
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDownloadAssets(
|
||||
didList: string[],
|
||||
tokenOrders: OrdersData[],
|
||||
chainIds: number[],
|
||||
cancelToken: CancelToken
|
||||
): Promise<DownloadedAsset[]> {
|
||||
const downloadedAssets: DownloadedAsset[] = []
|
||||
|
||||
try {
|
||||
const queryResult = await getAssetsFromDidList(
|
||||
didList,
|
||||
chainIds,
|
||||
cancelToken
|
||||
)
|
||||
const ddoList = queryResult?.results
|
||||
|
||||
for (let i = 0; i < tokenOrders?.length; i++) {
|
||||
const ddo = ddoList.filter(
|
||||
(ddo) =>
|
||||
tokenOrders[i].datatokenId.address.toLowerCase() ===
|
||||
ddo.dataToken.toLowerCase()
|
||||
)[0]
|
||||
|
||||
// make sure we are only pushing download orders
|
||||
if (ddo.service[1].type !== 'access') continue
|
||||
|
||||
downloadedAssets.push({
|
||||
ddo,
|
||||
networkId: ddo.chainId,
|
||||
dtSymbol: tokenOrders[i].datatokenId.symbol,
|
||||
timestamp: tokenOrders[i].timestamp
|
||||
})
|
||||
}
|
||||
|
||||
const sortedOrders = downloadedAssets.sort(
|
||||
(a, b) => b.timestamp - a.timestamp
|
||||
)
|
||||
return sortedOrders
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
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'
|
||||
@ -25,8 +25,9 @@ import {
|
||||
PoolShares_poolShares as PoolShare
|
||||
} from '../@types/apollo/PoolShares'
|
||||
import { BestPrice } from '../models/BestPrice'
|
||||
import { OrdersData_tokenOrders as OrdersData } from '../@types/apollo/OrdersData'
|
||||
|
||||
export interface UserTVL {
|
||||
export interface UserLiquidity {
|
||||
price: 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 {
|
||||
const config = getOceanConfig(chainId)
|
||||
return config.subgraphUri
|
||||
@ -604,7 +651,7 @@ export async function getAccountLiquidityInOwnAssets(
|
||||
accountId: string,
|
||||
chainIds: number[],
|
||||
pools: string[]
|
||||
): Promise<UserTVL> {
|
||||
): Promise<UserLiquidity> {
|
||||
const queryVariables = {
|
||||
user: accountId.toLowerCase(),
|
||||
pools: pools
|
||||
@ -630,3 +677,48 @@ export async function getAccountLiquidityInOwnAssets(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user