Various veOCEAN features (#1743)

* change top message, fixes, allocations

* lock ocean tooltip

* asset stats tweaks

* front page allocations refactor

* wording, run empty table messages through markdown

* footer stats tweaks

* profile tinkering

* fix allocations in table

* Tweak lock action logic

* fix allocation 0

* sort by allocation

* search ordering fixes

Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
mihaisc 2022-10-17 17:56:03 +03:00 committed by GitHub
parent 2fb4ee01c4
commit 67621f5639
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 447 additions and 49 deletions

View File

@ -14,7 +14,7 @@
"link": "/profile"
}
],
"announcement": "Explore [OceanONDA V4](https://blog.oceanprotocol.com/how-to-publish-a-data-nft-f58ad2a622a9).",
"announcement": "[Lock your OCEAN](https://df.oceandao.org/) to get veOCEAN, earn rewards and curate data.",
"warning": {
"ctd": "Compute-to-Data is still in a testing phase, please use it only on test networks."
}

View File

@ -7,7 +7,8 @@ export enum SortTermOptions {
Created = 'nft.created',
Relevance = '_score',
Orders = 'stats.orders',
Allocated = 'stats.allocated'
Allocated = 'stats.allocated',
Price = 'stats.price.value'
}
// Note: could not figure out how to get `enum` to be ambiant

View File

@ -215,6 +215,28 @@ export async function getAssetsFromDtList(
}
}
export async function getAssetsFromNftList(
nftList: string[],
chainIds: number[],
cancelToken: CancelToken
): Promise<Asset[]> {
try {
if (!(nftList.length > 0)) return
const baseParams = {
chainIds,
filters: [getFilterTerm('nftAddress', nftList)],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const queryResult = await queryMetadata(query, cancelToken)
return queryResult?.results
} catch (error) {
LoggerInstance.error(error.message)
}
}
export async function retrieveDDOListByDIDs(
didList: string[],
chainIds: number[],

View File

@ -1,7 +1,19 @@
import { AllLocked } from 'src/@types/subgraph/AllLocked'
import { OwnAllocations } from 'src/@types/subgraph/OwnAllocations'
import { NftOwnAllocation } from 'src/@types/subgraph/NftOwnAllocation'
import { OceanLocked } from 'src/@types/subgraph/OceanLocked'
import { gql, OperationResult } from 'urql'
import { fetchData, getQueryContext } from './subgraph'
import axios from 'axios'
import networkdata from '../../content/networks-metadata.json'
import {
getNetworkDataById,
getNetworkType,
NetworkType
} from '@hooks/useNetworkMetadata'
import { getAssetsFromNftList } from './aquarius'
import { chainIdsSupported } from 'app.config'
import { Asset, LoggerInstance } from '@oceanprotocol/lib'
const AllLocked = gql`
query AllLocked {
@ -11,10 +23,82 @@ const AllLocked = gql`
}
`
interface TotalVe {
const OwnAllocations = gql`
query OwnAllocations($address: String) {
veAllocations(where: { allocationUser: $address }) {
id
nftAddress
allocated
}
}
`
const NftOwnAllocation = gql`
query NftOwnAllocation($address: String, $nftAddress: String) {
veAllocations(
where: { allocationUser: $address, nftAddress: $nftAddress }
) {
allocated
}
}
`
const OceanLocked = gql`
query OceanLocked($address: String) {
veOCEAN(id: $address) {
id
lockedAmount
unlockTime
}
}
`
export interface TotalVe {
totalLocked: number
totalAllocated: number
}
export interface Allocation {
nftAddress: string
allocation: number
}
export interface AssetWithOwnAllocation {
asset: AssetExtended
allocation: string
}
export function getVeChainNetworkId(assetNetworkId: number): number {
const networkData = getNetworkDataById(networkdata, assetNetworkId)
const networkType = getNetworkType(networkData)
if (networkType === NetworkType.Mainnet) return 1
else return 5
}
export function getVeChainNetworkIds(assetNetworkIds: number[]): number[] {
const veNetworkIds: number[] = []
assetNetworkIds.forEach((x) => {
const id = getVeChainNetworkId(x)
veNetworkIds.indexOf(id) === -1 && veNetworkIds.push(id)
})
return veNetworkIds
}
export async function getNftOwnAllocation(
userAddress: string,
nftAddress: string,
networkId: number
): Promise<number> {
const veNetworkId = getVeChainNetworkId(networkId)
const queryContext = getQueryContext(veNetworkId)
const fetchedAllocation: OperationResult<NftOwnAllocation, any> =
await fetchData(
NftOwnAllocation,
{
address: userAddress.toLowerCase(),
nftAddress: nftAddress.toLowerCase()
},
queryContext
)
return fetchedAllocation.data?.veAllocations[0]?.allocated
}
export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
const totals = {
@ -26,7 +110,7 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
const response = await axios.post(`https://df-sql.oceandao.org/nftinfo`)
totals.totalAllocated = response.data?.reduce(
(previousValue: number, currentValue: { ve_allocated: any }) =>
(previousValue: number, currentValue: { ve_allocated: string }) =>
previousValue + Number(currentValue.ve_allocated),
0
)
@ -43,3 +127,66 @@ export async function getTotalAllocatedAndLocked(): Promise<TotalVe> {
)
return totals
}
export async function getLocked(
userAddress: string,
networkIds: number[]
): Promise<number> {
let total = 0
const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i])
const fetchedLocked: OperationResult<OceanLocked, any> = await fetchData(
OceanLocked,
{ address: userAddress.toLowerCase() },
queryContext
)
fetchedLocked.data?.veOCEAN?.lockedAmount &&
(total += Number(fetchedLocked.data?.veOCEAN?.lockedAmount))
}
return total
}
export async function getOwnAllocations(
networkIds: number[],
userAddress: string
): Promise<Allocation[]> {
const allocations: Allocation[] = []
const veNetworkIds = getVeChainNetworkIds(networkIds)
for (let i = 0; i < veNetworkIds.length; i++) {
const queryContext = getQueryContext(veNetworkIds[i])
const fetchedAllocations: OperationResult<OwnAllocations, any> =
await fetchData(
OwnAllocations,
{ address: userAddress.toLowerCase() },
queryContext
)
fetchedAllocations.data?.veAllocations.forEach(
(x) =>
x.allocated !== '0' &&
allocations.push({
nftAddress: x.nftAddress,
allocation: x.allocated / 100
})
)
}
return allocations
}
export async function getOwnAssetsWithAllocation(
networkIds: number[],
userAddress: string
): Promise<Asset[]> {
const allocations = await getOwnAllocations(networkIds, userAddress)
const assets = await getAssetsFromNftList(
allocations.map((x) => x.nftAddress),
chainIdsSupported,
null
)
return assets
}

View File

@ -18,7 +18,7 @@ export default function Conversion({
const { prices } = usePrices()
const { currency, locale } = useUserPreferences()
const [priceConverted, setPriceConverted] = useState('0.00')
const [priceConverted, setPriceConverted] = useState('0')
// detect fiat, only have those kick in full @coingecko/cryptoformat formatting
const isFiat = !isCrypto(currency)
// isCrypto() only checks for BTC & ETH & unknown but seems sufficient for now
@ -28,7 +28,7 @@ export default function Conversion({
const priceTokenId = getCoingeckoTokenId(symbol)
useEffect(() => {
if (!prices || !price || !priceTokenId || !prices[priceTokenId]) {
if (!prices || !priceTokenId || !prices[priceTokenId]) {
return
}
@ -41,7 +41,7 @@ export default function Conversion({
isFiat ? currency : '',
locale,
false,
{ decimalPlaces: 2 }
{ decimalPlaces: price === 0 ? 0 : 2 }
)
// It's a hack! Wrap everything in the string which is not a number or `.` or `,`
// with a span for consistent visual symbol formatting.

View File

@ -7,7 +7,7 @@
line-height: 1.2;
}
.price > div:firt-child {
.price > div:first-child {
white-space: nowrap;
}

View File

@ -1,6 +1,14 @@
import { markdownToHtml } from '@utils/markdown'
import React, { ReactElement } from 'react'
import styles from './Empty.module.css'
export default function Empty({ message }: { message?: string }): ReactElement {
return <div className={styles.empty}>{message || 'No results found'}</div>
return (
<div
className={styles.empty}
dangerouslySetInnerHTML={{
__html: markdownToHtml(message) || 'No results found'
}}
/>
)
}

View File

@ -1,44 +1,61 @@
import { useAsset } from '@context/Asset'
import { useUserPreferences } from '@context/UserPreferences'
import { useWeb3 } from '@context/Web3'
import Tooltip from '@shared/atoms/Tooltip'
import { formatPrice } from '@shared/Price/PriceUnit'
import { getNftOwnAllocation } from '@utils/veAllocation'
import React, { useEffect, useState } from 'react'
import styles from './index.module.css'
export default function AssetStats() {
const { locale } = useUserPreferences()
const { asset } = useAsset()
const [orders, setOrders] = useState(0)
const [allocated, setAllocated] = useState(0)
const { accountId } = useWeb3()
const [ownAllocation, setOwnAllocation] = useState(0)
useEffect(() => {
if (!asset) return
if (!asset || !accountId) return
const { orders, allocated } = asset.stats
setOrders(orders)
setAllocated(allocated)
}, [asset])
async function init() {
const allocation = await getNftOwnAllocation(
accountId,
asset.nftAddress,
asset.chainId
)
setOwnAllocation(allocation / 100)
}
init()
}, [accountId, asset])
return (
<footer className={styles.stats}>
{allocated && allocated > 0 ? (
{asset?.stats?.allocated && asset?.stats?.allocated > 0 ? (
<span className={styles.stat}>
<span className={styles.number}>
{formatPrice(allocated, locale)}
{formatPrice(asset.stats.allocated, locale)}
</span>
veOCEAN
</span>
) : null}
{!asset || !asset?.stats || orders < 0 ? (
{!asset?.stats || asset?.stats?.orders < 0 ? (
'N/A'
) : orders === 0 ? (
) : asset?.stats?.orders === 0 ? (
'No sales yet'
) : (
<span className={styles.stat}>
<span className={styles.number}>{orders}</span> sale
{orders === 1 ? '' : 's'}
<span className={styles.number}>{asset.stats.orders}</span> sale
{asset.stats.orders === 1 ? '' : 's'}
</span>
)}
{ownAllocation && ownAllocation > 0 ? (
<span className={styles.stat}>
<span className={styles.number}>{ownAllocation}</span>% allocated
<Tooltip
content={`You have ${ownAllocation}% of your total veOCEAN allocated to this asset.`}
/>
</span>
) : null}
</footer>
)
}

View File

@ -11,10 +11,7 @@ export default function MarketStatsTotal({
<>
<PriceUnit price={total.orders} size="small" /> orders across{' '}
<PriceUnit price={total.nfts} size="small" /> assets with{' '}
<PriceUnit price={total.datatokens} size="small" /> different datatokens.{' '}
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
allocated.{' '}
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.
<PriceUnit price={total.datatokens} size="small" /> different datatokens.
</>
)
}

View File

@ -15,6 +15,7 @@ import Tooltip from '@shared/atoms/Tooltip'
import Markdown from '@shared/Markdown'
import content from '../../../../content/footer.json'
import { getTotalAllocatedAndLocked } from '@utils/veAllocation'
import PriceUnit from '@shared/Price/PriceUnit'
const initialTotal: StatsTotal = {
nfts: 0,
@ -113,7 +114,7 @@ export default function MarketStats(): ReactElement {
return (
<div className={styles.stats}>
<>
<div>
<MarketStatsTotal total={total} />{' '}
<Tooltip
className={styles.info}
@ -121,7 +122,12 @@ export default function MarketStats(): ReactElement {
<Markdown className={styles.note} text={content.stats.note} />
}
/>
</>
</div>
<div>
<PriceUnit price={total.veLocked} symbol="OCEAN" size="small" /> locked.{' '}
<PriceUnit price={total.veAllocated} symbol="veOCEAN" size="small" />{' '}
allocated.
</div>
</div>
)
}

View File

@ -0,0 +1,47 @@
import React from 'react'
import Table, { TableOceanColumn } from '@shared/atoms/Table'
import AssetTitle from '@shared/AssetList/AssetListTitle'
import { AssetWithOwnAllocation } from '@utils/veAllocation'
const columns: TableOceanColumn<AssetWithOwnAllocation>[] = [
{
name: 'Dataset',
selector: (row) => {
const { metadata } = row.asset
return <AssetTitle title={metadata.name} asset={row.asset} />
},
maxWidth: '45rem',
grow: 1
},
{
name: 'Datatoken Symbol',
selector: (row) => row.asset.datatokens[0].symbol,
maxWidth: '10rem'
},
{
name: 'Allocated',
selector: (row) => row.allocation,
right: true,
sortable: true
}
]
export default function AssetListTable({
data,
isLoading
}: {
data: AssetWithOwnAllocation[]
isLoading: boolean
}) {
return (
<Table
columns={columns}
data={data}
defaultSortFieldId={3}
sortAsc={false}
isLoading={isLoading}
emptyMessage={`Your allocated assets will appear here. [Lock your OCEAN](https://df.oceandao.org) to get started.`}
noTableHead
/>
)
}

View File

@ -0,0 +1,3 @@
.section {
composes: section from '../index.module.css';
}

View File

@ -0,0 +1,93 @@
import React, { ReactElement, useEffect, useState } from 'react'
import { useWeb3 } from '@context/Web3'
import { AssetWithOwnAllocation, getOwnAllocations } from '@utils/veAllocation'
import styles from './index.module.css'
import {
getFilterTerm,
generateBaseQuery,
queryMetadata
} from '@utils/aquarius'
import { useUserPreferences } from '@context/UserPreferences'
import { useCancelToken } from '@hooks/useCancelToken'
import { useIsMounted } from '@hooks/useIsMounted'
import { LoggerInstance } from '@oceanprotocol/lib'
import AssetListTable from './AssetListTable'
export default function Allocations(): ReactElement {
const { accountId } = useWeb3()
const { chainIds } = useUserPreferences()
const isMounted = useIsMounted()
const newCancelToken = useCancelToken()
const [loading, setLoading] = useState<boolean>()
const [data, setData] = useState<AssetWithOwnAllocation[]>()
const [hasAllocations, setHasAllocations] = useState(false)
useEffect(() => {
if (!accountId) return
async function checkAllocations() {
try {
const allocations = await getOwnAllocations(chainIds, accountId)
setHasAllocations(allocations && allocations.length > 0)
} catch (error) {
LoggerInstance.error(error.message)
}
}
checkAllocations()
}, [accountId, chainIds])
useEffect(() => {
async function getAllocationAssets() {
if (!hasAllocations) return
try {
setLoading(true)
const allocations = await getOwnAllocations(chainIds, accountId)
setHasAllocations(allocations && allocations.length > 0)
const baseParams = {
chainIds,
filters: [
getFilterTerm(
'nftAddress',
allocations.map((x) => x.nftAddress)
)
],
ignorePurgatory: true
} as BaseQueryParams
const query = generateBaseQuery(baseParams)
const result = await queryMetadata(query, newCancelToken())
const assetsWithAllocation: AssetWithOwnAllocation[] = []
result?.results.forEach((asset) => {
const allocation = allocations.find(
(x) => x.nftAddress.toLowerCase() === asset.nftAddress.toLowerCase()
)
assetsWithAllocation.push({
asset,
allocation: `${allocation.allocation} %`
})
})
if (!isMounted()) return
setData(assetsWithAllocation)
setLoading(false)
} catch (error) {
LoggerInstance.error(error.message)
}
}
getAllocationAssets()
}, [hasAllocations, accountId, chainIds, isMounted, newCancelToken])
return (
<section className={styles.section}>
<h3>Your Allocated Assets</h3>
<AssetListTable data={data} isLoading={loading} />
</section>
)
}

View File

@ -8,12 +8,14 @@ import TopSales from './TopSales'
import TopTags from './TopTags'
import SectionQueryResult from './SectionQueryResult'
import styles from './index.module.css'
import Allocations from './Allocations'
export default function HomePage(): ReactElement {
const { chainIds } = useUserPreferences()
const [queryLatest, setQueryLatest] = useState<SearchQuery>()
const [queryMostSales, setQueryMostSales] = useState<SearchQuery>()
const [queryMostAllocation, setQueryMostAllocation] = useState<SearchQuery>()
const { chainIds } = useUserPreferences()
useEffect(() => {
const baseParams = {
@ -52,10 +54,12 @@ export default function HomePage(): ReactElement {
return (
<>
<section className={styles.section}>
<h3>Bookmarks</h3>
<h3>Your Bookmarks</h3>
<Bookmarks />
</section>
<Allocations />
<SectionQueryResult
title="Highest veOCEAN Allocations"
query={queryMostAllocation}

View File

@ -9,7 +9,7 @@
.number {
white-space: nowrap;
display: inline-flex;
align-items: center;
align-items: baseline;
line-height: 1;
}

View File

@ -4,7 +4,7 @@ import Tooltip from '@shared/atoms/Tooltip'
import styles from './NumberUnit.module.css'
interface NumberUnitProps {
label: string
label: string | ReactElement
value: number | string | Element | ReactElement
small?: boolean
icon?: Element | ReactElement

View File

@ -4,3 +4,24 @@
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
margin-top: var(--spacer);
}
.stats [class^='PriceUnit_symbol'] {
color: var(--color-secondary);
font-weight: var(--font-weight-base);
font-size: var(--font-size-small);
}
.stats .link,
.stats .link:hover {
color: var(--color-primary);
font-size: var(--font-size-small);
text-decoration: none;
padding: 0;
border: 0;
}
.stats [class^='PriceUnit_price'] {
color: var(--color-secondary);
font-weight: var(--font-weight-base);
font-size: var(--font-size-small);
}

View File

@ -6,16 +6,31 @@ import NumberUnit from './NumberUnit'
import styles from './Stats.module.css'
import { useProfile } from '@context/Profile'
import { getAccessDetailsForAssets } from '@utils/accessDetailsAndPricing'
import { getLocked } from '@utils/veAllocation'
import PriceUnit from '@shared/Price/PriceUnit'
import Button from '@shared/atoms/Button'
import { useWeb3 } from '@context/Web3'
export default function Stats({
accountId
}: {
accountId: string
}): ReactElement {
const web3 = useWeb3()
const { chainIds } = useUserPreferences()
const { assets, assetsTotal, sales } = useProfile()
const [totalSales, setTotalSales] = useState(0)
const [lockedOcean, setLockedOcean] = useState(0)
useEffect(() => {
async function getLockedOcean() {
if (!accountId) return
const locked = await getLocked(accountId, chainIds)
setLockedOcean(locked)
}
getLockedOcean()
}, [accountId, chainIds])
useEffect(() => {
if (!assets || !accountId || !chainIds) return
@ -59,6 +74,30 @@ export default function Stats({
value={sales < 0 ? 0 : sales}
/>
<NumberUnit label="Published" value={assetsTotal} />
<NumberUnit
label={
lockedOcean === 0 && accountId === web3.accountId ? (
<Button
className={styles.link}
style="text"
href="https://df.oceandao.org"
>
Lock OCEAN
</Button>
) : (
<>
<PriceUnit price={lockedOcean} symbol="OCEAN" /> locked
</>
)
}
value={
<Conversion
price={lockedOcean > 0 ? lockedOcean : 0}
symbol="OCEAN"
hideApproximateSymbol
/>
}
/>
</div>
)
}

View File

@ -3,13 +3,11 @@ div.filterList {
white-space: normal;
margin-top: 0;
margin-bottom: 0;
display: flex;
gap: calc(var(--spacer) / 4) calc(var(--spacer) / 2);
flex-direction: column;
align-items: baseline;
}
.filter {
.filter,
.filterList > div {
display: inline-block;
}

View File

@ -1,10 +1,8 @@
.row {
display: inline-flex;
flex-direction: column;
align-items: stretch;
justify-content: space-between;
width: 100%;
white-space: nowrap;
margin-bottom: calc(var(--spacer) / 2);
}

View File

@ -8,15 +8,9 @@
overflow-y: auto;
}
@media (min-width: 55rem) {
.sortList {
align-self: flex-end;
overflow-y: unset;
}
}
.sortLabel {
composes: label from '@shared/FormInput/Label.module.css';
padding: 0;
margin-bottom: 0;
margin-left: calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 1.5);
@ -26,7 +20,7 @@
}
.sorted {
display: flex;
display: inline-block;
padding: calc(var(--spacer) / 6) calc(var(--spacer) / 2);
margin-right: calc(var(--spacer) / 4);
color: var(--color-secondary);
@ -35,6 +29,7 @@
font-weight: var(--font-weight-base);
background: var(--background-content);
box-shadow: none;
white-space: nowrap;
}
.sorted:hover,
@ -47,7 +42,6 @@
}
.direction {
display: flex;
background: transparent;
border: none;
color: inherit;

View File

@ -13,7 +13,10 @@ const cx = classNames.bind(styles)
const sortItems = [
{ display: 'Relevance', value: SortTermOptions.Relevance },
{ display: 'Published', value: SortTermOptions.Created }
{ display: 'Published', value: SortTermOptions.Created },
{ display: 'Sales', value: SortTermOptions.Orders },
{ display: 'Total allocation', value: SortTermOptions.Allocated },
{ display: 'Price', value: SortTermOptions.Price }
]
export default function Sort({