From 268cba572ca30b86f7df19c1918f4853dba8c788 Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Fri, 20 Oct 2023 16:10:19 +0200
Subject: [PATCH 1/9] prime pages
---
.nvmrc | 2 +-
.../src/assets/images/aave-token-logo.svg | 1 +
.../src/components/DebugFlags/config.ts | 6 +
.../src/components/LayoutBase/BasePadding.tsx | 2 +-
.../components/LayoutBase/LayoutSection.tsx | 43 ++++++
centrifuge-app/src/components/Menu/index.tsx | 9 +-
centrifuge-app/src/components/Root.tsx | 8 ++
centrifuge-app/src/pages/Prime/Detail.tsx | 15 +++
centrifuge-app/src/pages/Prime/index.tsx | 127 ++++++++++++++++++
9 files changed, 210 insertions(+), 3 deletions(-)
create mode 100644 centrifuge-app/src/assets/images/aave-token-logo.svg
create mode 100644 centrifuge-app/src/components/LayoutBase/LayoutSection.tsx
create mode 100644 centrifuge-app/src/pages/Prime/Detail.tsx
create mode 100644 centrifuge-app/src/pages/Prime/index.tsx
diff --git a/.nvmrc b/.nvmrc
index 6f7f377bf5..3f430af82b 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-v16
+v18
diff --git a/centrifuge-app/src/assets/images/aave-token-logo.svg b/centrifuge-app/src/assets/images/aave-token-logo.svg
new file mode 100644
index 0000000000..d784fbe0a4
--- /dev/null
+++ b/centrifuge-app/src/assets/images/aave-token-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/centrifuge-app/src/components/DebugFlags/config.ts b/centrifuge-app/src/components/DebugFlags/config.ts
index 714a67508f..f62aee2939 100644
--- a/centrifuge-app/src/components/DebugFlags/config.ts
+++ b/centrifuge-app/src/components/DebugFlags/config.ts
@@ -47,6 +47,7 @@ export type Key =
| 'showPodAccountCreation'
| 'convertEvmAddress'
| 'showPortfolio'
+ | 'showPrime'
| 'poolCreationType'
export const flagsConfig: Record = {
@@ -121,6 +122,11 @@ export const flagsConfig: Record = {
default: false,
alwaysShow: true,
},
+ showPrime: {
+ type: 'checkbox',
+ default: false,
+ alwaysShow: true,
+ },
poolCreationType: {
type: 'select',
default: config.poolCreationType || 'immediate',
diff --git a/centrifuge-app/src/components/LayoutBase/BasePadding.tsx b/centrifuge-app/src/components/LayoutBase/BasePadding.tsx
index 05e1dbe02a..9c9ef1b4b6 100644
--- a/centrifuge-app/src/components/LayoutBase/BasePadding.tsx
+++ b/centrifuge-app/src/components/LayoutBase/BasePadding.tsx
@@ -7,7 +7,7 @@ type BaseSectionProps = BoxProps & {
export function BasePadding({ children, ...boxProps }: BaseSectionProps) {
return (
-
+
{children}
)
diff --git a/centrifuge-app/src/components/LayoutBase/LayoutSection.tsx b/centrifuge-app/src/components/LayoutBase/LayoutSection.tsx
new file mode 100644
index 0000000000..237a2a1ff2
--- /dev/null
+++ b/centrifuge-app/src/components/LayoutBase/LayoutSection.tsx
@@ -0,0 +1,43 @@
+import { Box, BoxProps, Shelf, Stack, Text } from '@centrifuge/fabric'
+import * as React from 'react'
+import { BasePadding } from './BasePadding'
+
+type Props = {
+ title?: React.ReactNode
+ titleAddition?: React.ReactNode
+ subtitle?: string
+ headerRight?: React.ReactNode
+ children: React.ReactNode
+} & BoxProps
+
+export function LayoutSection({ title, titleAddition, subtitle, headerRight, children, ...boxProps }: Props) {
+ return (
+
+ {(title || titleAddition || subtitle || headerRight) && (
+
+
+ {(title || titleAddition) && (
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {titleAddition}
+
+
+ )}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+ {headerRight}
+
+ )}
+ {children}
+
+ )
+}
diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx
index 8d7a0fdc2d..55166bc069 100644
--- a/centrifuge-app/src/components/Menu/index.tsx
+++ b/centrifuge-app/src/components/Menu/index.tsx
@@ -1,5 +1,6 @@
import {
Box,
+ IconGlobe,
IconInvestments,
IconNft,
IconPieChart,
@@ -23,7 +24,7 @@ export function Menu() {
const pools = usePoolsThatAnyConnectedAddressHasPermissionsFor() || []
const isLarge = useIsAboveBreakpoint('L')
const address = useAddress('substrate')
- const { showPortfolio } = useDebugFlags()
+ const { showPortfolio, showPrime } = useDebugFlags()
return (
)}
+ {showPrime && address && (
+
+
+ Prime
+
+ )}
{(pools.length > 0 || config.poolCreationType === 'immediate') && (
diff --git a/centrifuge-app/src/components/Root.tsx b/centrifuge-app/src/components/Root.tsx
index abddbf6a8b..71a78bc70c 100644
--- a/centrifuge-app/src/components/Root.tsx
+++ b/centrifuge-app/src/components/Root.tsx
@@ -33,6 +33,8 @@ import { PoolDetailPage } from '../pages/Pool'
import { PoolsPage } from '../pages/Pools'
import { PortfolioPage } from '../pages/Portfolio'
import { TransactionsPage } from '../pages/Portfolio/Transactions'
+import { PrimePage } from '../pages/Prime'
+import { PrimeDetailPage } from '../pages/Prime/Detail'
import { TokenOverviewPage } from '../pages/Tokens'
import { pinToApi } from '../utils/pinToApi'
import { DebugFlags, initialFlagsState } from './DebugFlags'
@@ -217,6 +219,12 @@ function Routes() {
+
+
+
+
+
+
diff --git a/centrifuge-app/src/pages/Prime/Detail.tsx b/centrifuge-app/src/pages/Prime/Detail.tsx
new file mode 100644
index 0000000000..186f823fad
--- /dev/null
+++ b/centrifuge-app/src/pages/Prime/Detail.tsx
@@ -0,0 +1,15 @@
+import { useParams } from 'react-router'
+import { LayoutBase } from '../../components/LayoutBase'
+
+export function PrimeDetailPage() {
+ return (
+
+
+
+ )
+}
+
+function PrimeDetail() {
+ const { dao } = useParams<{ dao: string }>()
+ return Prime detail, {dao}
+}
diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx
new file mode 100644
index 0000000000..60f2beca24
--- /dev/null
+++ b/centrifuge-app/src/pages/Prime/index.tsx
@@ -0,0 +1,127 @@
+import { Network, useCentrifuge, useCentrifugeUtils, useGetNetworkName } from '@centrifuge/centrifuge-react'
+import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric'
+import { useQuery } from 'react-query'
+import { firstValueFrom } from 'rxjs'
+import aaveLogo from '../../assets/images/aave-token-logo.svg'
+import { Column, DataTable, SortableTableHeader } from '../../components/DataTable'
+import { LayoutBase } from '../../components/LayoutBase'
+import { LayoutSection } from '../../components/LayoutBase/LayoutSection'
+import { formatBalance, formatPercentage } from '../../utils/formatting'
+
+type DAO = {
+ slug: string
+ name: string
+ network: Network
+ address: string
+ icon: string
+}
+
+const DAOs: DAO[] = [
+ {
+ slug: 'aave',
+ name: 'Aave',
+ network: 1,
+ address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f',
+ icon: aaveLogo,
+ },
+]
+
+export function PrimePage() {
+ return (
+
+
+
+ )
+}
+
+function Prime() {
+ return (
+ <>
+
+ Centrifuge Prime
+
+ Centrifuge Prime was built to meet the needs of large decentralized organizations and protocols. Through
+ Centrifuge Prime, DeFi native organizations can integrate with the largest financial markets in the world and
+ take advantage of real yields from real economic activity - all onchain. Assets tailored to your needs,
+ processes adapted to your governance, and all through decentralized rails.
+
+
+
+ Go to website
+
+
+
+
+ >
+ )
+}
+
+type Row = DAO & { value?: number; networkName: string }
+
+export const columns: Column[] = [
+ {
+ align: 'left',
+ header: 'DAO',
+ cell: (row: Row) => (
+
+
+ {row.name}
+
+ ),
+ flex: '1',
+ },
+ {
+ align: 'left',
+ header: 'Network',
+ cell: (row: Row) => {row.networkName},
+ flex: '3',
+ },
+ {
+ header: ,
+ cell: (row: Row) => (
+ {row.value && formatBalance(row.value, 'USD')}
+ ),
+ flex: '3',
+ sortKey: 'value',
+ },
+ {
+ header: 'Profit',
+ cell: (row: Row) => (
+ {row.value && formatPercentage(row.value)}
+ ),
+ flex: '3',
+ },
+]
+
+function DaoPortfoliosTable() {
+ const utils = useCentrifugeUtils()
+ const cent = useCentrifuge()
+ const getNetworkName = useGetNetworkName()
+ const { data } = useQuery(['daoPortfolios'], async () => {
+ const result = await Promise.all(
+ DAOs.map((dao) => {
+ const address =
+ typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address
+ return firstValueFrom(cent.pools.getBalances([address]))
+ })
+ )
+ return result
+ })
+
+ const rows: Row[] = DAOs.map((dao, i) => ({
+ ...dao,
+ value: data?.[i].native.balance.toFloat(),
+ networkName: getNetworkName(dao.network),
+ }))
+
+ return (
+
+ `/prime/${row.slug}`} />
+
+ )
+}
From 6661c87f4789dd77d6db71d1d358ad75b7ad254f Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Fri, 20 Oct 2023 16:11:01 +0200
Subject: [PATCH 2/9] table
---
centrifuge-app/src/pages/Prime/index.tsx | 72 ++++++++++++------------
1 file changed, 37 insertions(+), 35 deletions(-)
diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx
index 60f2beca24..fb824b0f3a 100644
--- a/centrifuge-app/src/pages/Prime/index.tsx
+++ b/centrifuge-app/src/pages/Prime/index.tsx
@@ -63,41 +63,6 @@ function Prime() {
type Row = DAO & { value?: number; networkName: string }
-export const columns: Column[] = [
- {
- align: 'left',
- header: 'DAO',
- cell: (row: Row) => (
-
-
- {row.name}
-
- ),
- flex: '1',
- },
- {
- align: 'left',
- header: 'Network',
- cell: (row: Row) => {row.networkName},
- flex: '3',
- },
- {
- header: ,
- cell: (row: Row) => (
- {row.value && formatBalance(row.value, 'USD')}
- ),
- flex: '3',
- sortKey: 'value',
- },
- {
- header: 'Profit',
- cell: (row: Row) => (
- {row.value && formatPercentage(row.value)}
- ),
- flex: '3',
- },
-]
-
function DaoPortfoliosTable() {
const utils = useCentrifugeUtils()
const cent = useCentrifuge()
@@ -119,6 +84,43 @@ function DaoPortfoliosTable() {
networkName: getNetworkName(dao.network),
}))
+ const uniqueNetworks = [...new Set(DAOs.map((dao) => dao.network))]
+
+ const columns: Column[] = [
+ {
+ align: 'left',
+ header: 'DAO',
+ cell: (row: Row) => (
+
+
+ {row.name}
+
+ ),
+ flex: '1',
+ },
+ {
+ align: 'left',
+ header: 'Network',
+ cell: (row: Row) => {row.networkName},
+ flex: '3',
+ },
+ {
+ header: ,
+ cell: (row: Row) => (
+ {row.value && formatBalance(row.value, 'USD')}
+ ),
+ flex: '3',
+ sortKey: 'value',
+ },
+ {
+ header: 'Profit',
+ cell: (row: Row) => (
+ {row.value && formatPercentage(row.value)}
+ ),
+ flex: '3',
+ },
+ ]
+
return (
`/prime/${row.slug}`} />
From 7786ec2392e8c3a53809adacd2314e01443931f7 Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Tue, 24 Oct 2023 10:03:41 +0200
Subject: [PATCH 3/9] filterable table header
---
centrifuge-app/src/components/DataTable.tsx | 131 +++++++++++++++++-
.../src/components/PoolFilter/styles.ts | 5 +
centrifuge-app/src/pages/Prime/index.tsx | 25 +++-
centrifuge-app/src/utils/useFilters.ts | 73 ++++++++++
fabric/src/components/FileUpload/index.tsx | 2 +-
fabric/src/components/ImageUpload/index.tsx | 2 +-
.../src/components/InteractiveCard/index.tsx | 2 +-
.../components/Pagination/usePagination.ts | 4 +-
fabric/src/index.ts | 3 +-
fabric/src/utils/useControlledState.ts | 6 +-
fabric/src/utils/useEventCallback.ts | 4 +-
11 files changed, 237 insertions(+), 20 deletions(-)
create mode 100644 centrifuge-app/src/utils/useFilters.ts
diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx
index 086635ba8f..f691622456 100644
--- a/centrifuge-app/src/components/DataTable.tsx
+++ b/centrifuge-app/src/components/DataTable.tsx
@@ -1,10 +1,25 @@
-import { Card, IconArrowDown, Shelf, Stack, Text } from '@centrifuge/fabric'
+import {
+ Box,
+ Card,
+ Checkbox,
+ Divider,
+ IconArrowDown,
+ IconFilter,
+ Menu,
+ Popover,
+ Shelf,
+ Stack,
+ Text,
+ Tooltip,
+} from '@centrifuge/fabric'
import css from '@styled-system/css'
import BN from 'bn.js'
import * as React from 'react'
import { Link, LinkProps } from 'react-router-dom'
import styled from 'styled-components'
import { useElementScrollSize } from '../utils/useElementScrollSize'
+import { FiltersState } from '../utils/useFilters'
+import { FilterButton, QuickAction } from './PoolFilter/styles'
type GroupedProps = {
groupIndex?: number
@@ -230,11 +245,15 @@ const HeaderCol = styled(DataCol)`
align-items: center;
`
-export const SortableTableHeader: React.VFC<{ label: string; orderBy?: OrderBy; align?: Column['align'] }> = ({
+export function SortableTableHeader({
label,
orderBy,
align,
-}) => {
+}: {
+ label: string
+ orderBy?: OrderBy
+ align?: Column['align']
+}) {
return (
{(!align || align === 'right') && (
@@ -256,6 +275,112 @@ export const SortableTableHeader: React.VFC<{ label: string; orderBy?: OrderBy;
)
}
+export function FilterableTableHeader({
+ filterKey: key,
+ label,
+ options,
+ filters,
+ align,
+ tooltip,
+}: {
+ filterKey: string
+ label: string
+ options: string[] | Record
+ filters: FiltersState
+ align?: Column['align']
+ tooltip?: string
+}) {
+ const optionKeys = Array.isArray(options) ? options : Object.keys(options)
+ const form = React.useRef(null)
+
+ function handleChange() {
+ if (!form.current) return
+ const formData = new FormData(form.current)
+ const entries = formData.getAll(key) as string[]
+ console.log('entries', entries)
+ filters.setFilter(key, entries)
+ }
+
+ function deselectAll() {
+ filters.setFilter(key, [])
+ }
+
+ function selectAll() {
+ filters.setFilter(key, optionKeys)
+ }
+ const state = filters.getState()
+ const selectedOptions = state[key] as Set | undefined
+ console.log('state', state)
+
+ return (
+
+ {
+ return (
+
+ {tooltip ? (
+
+
+ {label}
+
+
+
+ ) : (
+
+ {label}
+
+
+ )}
+
+ )
+ }}
+ renderContent={(props, ref) => (
+
+
+
+ )}
+ />
+
+ )
+}
+
const StyledHeader = styled(Shelf)`
color: ${({ theme }) => theme.colors.textSecondary};
diff --git a/centrifuge-app/src/components/PoolFilter/styles.ts b/centrifuge-app/src/components/PoolFilter/styles.ts
index f94d6c7d85..ebc2af22df 100644
--- a/centrifuge-app/src/components/PoolFilter/styles.ts
+++ b/centrifuge-app/src/components/PoolFilter/styles.ts
@@ -12,6 +12,11 @@ const sharedStyles = css`
outline: ${({ theme }) => `2px solid ${theme.colors.textSelected}`};
outline-offset: 4px;
}
+ &:hover,
+ &:hover > svg {
+ cursor: pointer;
+ color: ${({ theme }) => theme.colors.textInteractiveHover};
+ }
`
export const FilterButton = styled(Text)`
diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx
index fb824b0f3a..3394920b9c 100644
--- a/centrifuge-app/src/pages/Prime/index.tsx
+++ b/centrifuge-app/src/pages/Prime/index.tsx
@@ -3,10 +3,11 @@ import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder }
import { useQuery } from 'react-query'
import { firstValueFrom } from 'rxjs'
import aaveLogo from '../../assets/images/aave-token-logo.svg'
-import { Column, DataTable, SortableTableHeader } from '../../components/DataTable'
+import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from '../../components/DataTable'
import { LayoutBase } from '../../components/LayoutBase'
import { LayoutSection } from '../../components/LayoutBase/LayoutSection'
import { formatBalance, formatPercentage } from '../../utils/formatting'
+import { useFilters } from '../../utils/useFilters'
type DAO = {
slug: string
@@ -24,6 +25,13 @@ const DAOs: DAO[] = [
address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f',
icon: aaveLogo,
},
+ {
+ slug: 'gnosis',
+ name: 'Gnosis',
+ network: 5,
+ address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f',
+ icon: aaveLogo,
+ },
]
export function PrimePage() {
@@ -78,13 +86,15 @@ function DaoPortfoliosTable() {
return result
})
- const rows: Row[] = DAOs.map((dao, i) => ({
+ const mapped: Row[] = DAOs.map((dao, i) => ({
...dao,
value: data?.[i].native.balance.toFloat(),
networkName: getNetworkName(dao.network),
}))
const uniqueNetworks = [...new Set(DAOs.map((dao) => dao.network))]
+ const filters = useFilters({ data: mapped })
+ console.log('filters.data', filters.data)
const columns: Column[] = [
{
@@ -100,7 +110,14 @@ function DaoPortfoliosTable() {
},
{
align: 'left',
- header: 'Network',
+ header: (
+ [chain, getNetworkName(chain)]))}
+ />
+ ),
cell: (row: Row) => {row.networkName},
flex: '3',
},
@@ -123,7 +140,7 @@ function DaoPortfoliosTable() {
return (
- `/prime/${row.slug}`} />
+ `/prime/${row.slug}`} />
)
}
diff --git a/centrifuge-app/src/utils/useFilters.ts b/centrifuge-app/src/utils/useFilters.ts
new file mode 100644
index 0000000000..cce76c3565
--- /dev/null
+++ b/centrifuge-app/src/utils/useFilters.ts
@@ -0,0 +1,73 @@
+import { useEventCallback } from '@centrifuge/fabric'
+import get from 'lodash/get'
+import * as React from 'react'
+import { useHistory, useLocation } from 'react-router'
+
+export type PaginationProps = {
+ key?: string // In case more than one table on the page uses search params for filters
+ data?: T[]
+ useSearchParams?: boolean
+}
+
+export function useFilters({ key: prefix = 'f_', data = [], useSearchParams = true }: PaginationProps = {}) {
+ const history = useHistory()
+ const { search } = useLocation()
+ const [params, setParams] = React.useState(() => new URLSearchParams(useSearchParams ? search : undefined))
+
+ const setFilter = useEventCallback((key: string, value: string[]) => {
+ setParams((prev) => {
+ const params = new URLSearchParams(prev)
+ params.delete(prefix + key)
+ value.forEach((value, i) => {
+ if (i === 0) {
+ params.set(prefix + key, value)
+ } else {
+ params.append(prefix + key, value)
+ }
+ })
+ return params
+ })
+ })
+
+ React.useEffect(() => {
+ history.replace({ search: params.toString() })
+ }, [params, history])
+
+ const state: Record> = {}
+
+ const hasFilter = useEventCallback((key: string, value: string) => {
+ return !!state[key]?.has(String(value))
+ })
+
+ const getState = useGetLatest(state)
+
+ for (const [prefixedKey, value] of params.entries()) {
+ if (!prefixedKey.startsWith(prefix)) continue
+ const key = prefixedKey.slice(prefix.length)
+ if (state[key]) {
+ state[key].add(value)
+ } else {
+ state[key] = new Set([value])
+ }
+ }
+
+ const entries = Object.entries(state)
+ const filtered = data.filter((entry) => entries.every(([key, set]) => set.has(String(get(entry, key)))))
+
+ return {
+ setFilter,
+ hasFilter,
+ data: filtered,
+ state,
+ getState,
+ }
+}
+
+export type FiltersState = ReturnType
+
+function useGetLatest(obj: T): () => T {
+ const ref = React.useRef(obj)
+ ref.current = obj
+
+ return React.useState(() => () => ref.current)[0]
+}
diff --git a/fabric/src/components/FileUpload/index.tsx b/fabric/src/components/FileUpload/index.tsx
index 297d718bec..ad403277eb 100644
--- a/fabric/src/components/FileUpload/index.tsx
+++ b/fabric/src/components/FileUpload/index.tsx
@@ -5,7 +5,7 @@ import IconAlertCircle from '../../icon/IconAlertCircle'
import IconSpinner from '../../icon/IconSpinner'
import IconUpload from '../../icon/IconUpload'
import IconX from '../../icon/IconX'
-import useControlledState from '../../utils/useControlledState'
+import { useControlledState } from '../../utils/useControlledState'
import { Box } from '../Box'
import { Button } from '../Button'
import { Flex } from '../Flex'
diff --git a/fabric/src/components/ImageUpload/index.tsx b/fabric/src/components/ImageUpload/index.tsx
index d4d4f8861e..cceac9b8f5 100644
--- a/fabric/src/components/ImageUpload/index.tsx
+++ b/fabric/src/components/ImageUpload/index.tsx
@@ -4,7 +4,7 @@ import { ResponsiveValue } from 'styled-system'
import IconUpload from '../../icon/IconUpload'
import IconX from '../../icon/IconX'
import { Size } from '../../utils/types'
-import useControlledState from '../../utils/useControlledState'
+import { useControlledState } from '../../utils/useControlledState'
import { Box } from '../Box'
import { Button } from '../Button'
import { Flex } from '../Flex'
diff --git a/fabric/src/components/InteractiveCard/index.tsx b/fabric/src/components/InteractiveCard/index.tsx
index 341776c061..b2aeaacb15 100644
--- a/fabric/src/components/InteractiveCard/index.tsx
+++ b/fabric/src/components/InteractiveCard/index.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import styled, { useTheme } from 'styled-components'
import { IconChevronRight } from '../../icon'
-import useControlledState from '../../utils/useControlledState'
+import { useControlledState } from '../../utils/useControlledState'
import { Box } from '../Box'
import { VisualButton } from '../Button'
import { Card, CardProps } from '../Card'
diff --git a/fabric/src/components/Pagination/usePagination.ts b/fabric/src/components/Pagination/usePagination.ts
index 8936f8d404..3c80f3192c 100644
--- a/fabric/src/components/Pagination/usePagination.ts
+++ b/fabric/src/components/Pagination/usePagination.ts
@@ -1,6 +1,6 @@
import * as React from 'react'
-import useControlledState from '../../utils/useControlledState'
-import useEventCallback from '../../utils/useEventCallback'
+import { useControlledState } from '../../utils/useControlledState'
+import { useEventCallback } from '../../utils/useEventCallback'
export type PaginationProps = {
manual?: boolean
diff --git a/fabric/src/index.ts b/fabric/src/index.ts
index d8811ce946..303a1a3029 100644
--- a/fabric/src/index.ts
+++ b/fabric/src/index.ts
@@ -46,4 +46,5 @@ export * from './components/Tooltip'
export * from './icon/index'
export * from './theme'
export { mapResponsive, toPx } from './utils/styled'
-export { default as useControlledState } from './utils/useControlledState'
+export { useControlledState } from './utils/useControlledState'
+export { useEventCallback } from './utils/useEventCallback'
diff --git a/fabric/src/utils/useControlledState.ts b/fabric/src/utils/useControlledState.ts
index 6903a82590..fee802bcfa 100644
--- a/fabric/src/utils/useControlledState.ts
+++ b/fabric/src/utils/useControlledState.ts
@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction, useState } from 'react'
-import useEventCallback from './useEventCallback'
+import { useEventCallback } from './useEventCallback'
-function useControlledState | Dispatch>>(
+export function useControlledState | Dispatch>>(
initialUncontrolledValue: T,
externalValue?: T,
setExternalValue?: D
@@ -17,5 +17,3 @@ function useControlledState | Dispatch>>(
return [value, setValue as any]
}
-
-export default useControlledState
diff --git a/fabric/src/utils/useEventCallback.ts b/fabric/src/utils/useEventCallback.ts
index 12904fff7c..e6a1344768 100644
--- a/fabric/src/utils/useEventCallback.ts
+++ b/fabric/src/utils/useEventCallback.ts
@@ -2,7 +2,7 @@ import { useCallback, useLayoutEffect, useRef } from 'react'
type Calback = (...args: any[]) => any
-function useEventCallback(callback: T) {
+export function useEventCallback(callback: T) {
const ref = useRef((() => {
throw new Error('Cannot call an event handler while rendering.')
}) as any)
@@ -16,5 +16,3 @@ function useEventCallback(callback: T) {
return fn(...args)
}, [])
}
-
-export default useEventCallback
From 3f0dd3189c8c0d00df987fe05a85bed9c881f30d Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Tue, 24 Oct 2023 13:09:53 +0200
Subject: [PATCH 4/9] profit
---
centrifuge-app/src/pages/Prime/index.tsx | 136 +++++++++++++++++------
centrifuge-app/src/utils/useFilters.ts | 31 +++---
centrifuge-app/src/utils/useSubquery.ts | 10 ++
3 files changed, 126 insertions(+), 51 deletions(-)
create mode 100644 centrifuge-app/src/utils/useSubquery.ts
diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx
index 3394920b9c..96be8fe4f7 100644
--- a/centrifuge-app/src/pages/Prime/index.tsx
+++ b/centrifuge-app/src/pages/Prime/index.tsx
@@ -1,3 +1,4 @@
+import { Price } from '@centrifuge/centrifuge-js'
import { Network, useCentrifuge, useCentrifugeUtils, useGetNetworkName } from '@centrifuge/centrifuge-react'
import { AnchorButton, Box, IconExternalLink, Shelf, Text, TextWithPlaceholder } from '@centrifuge/fabric'
import { useQuery } from 'react-query'
@@ -6,8 +7,10 @@ import aaveLogo from '../../assets/images/aave-token-logo.svg'
import { Column, DataTable, FilterableTableHeader, SortableTableHeader } from '../../components/DataTable'
import { LayoutBase } from '../../components/LayoutBase'
import { LayoutSection } from '../../components/LayoutBase/LayoutSection'
+import { formatDate } from '../../utils/date'
import { formatBalance, formatPercentage } from '../../utils/formatting'
import { useFilters } from '../../utils/useFilters'
+import { useSubquery } from '../../utils/useSubquery'
type DAO = {
slug: string
@@ -21,15 +24,8 @@ const DAOs: DAO[] = [
{
slug: 'aave',
name: 'Aave',
- network: 1,
- address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f',
- icon: aaveLogo,
- },
- {
- slug: 'gnosis',
- name: 'Gnosis',
- network: 5,
- address: '0x423420Ae467df6e90291fd0252c0A8a637C1e03f',
+ network: 'centrifuge',
+ address: 'kALNreUp6oBmtfG87fe7MakWR8BnmQ4SmKjjfG27iVd3nuTue',
icon: aaveLogo,
},
]
@@ -47,12 +43,14 @@ function Prime() {
<>
Centrifuge Prime
-
- Centrifuge Prime was built to meet the needs of large decentralized organizations and protocols. Through
- Centrifuge Prime, DeFi native organizations can integrate with the largest financial markets in the world and
- take advantage of real yields from real economic activity - all onchain. Assets tailored to your needs,
- processes adapted to your governance, and all through decentralized rails.
-
+
+
+ Centrifuge Prime was built to meet the needs of large decentralized organizations and protocols. Through
+ Centrifuge Prime, DeFi native organizations can integrate with the largest financial markets in the world
+ and take advantage of real yields from real economic activity - all onchain. Assets tailored to your needs,
+ processes adapted to your governance, and all through decentralized rails.
+
+
{
- const result = await Promise.all(
- DAOs.map((dao) => {
- const address =
- typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address
- return firstValueFrom(cent.pools.getBalances([address]))
- })
- )
- return result
- })
- const mapped: Row[] = DAOs.map((dao, i) => ({
+ const daos = DAOs.map((dao) => ({
...dao,
- value: data?.[i].native.balance.toFloat(),
- networkName: getNetworkName(dao.network),
+ address: utils.formatAddress(
+ typeof dao.network === 'number' ? utils.evmToSubstrateAddress(dao.address, dao.network) : dao.address
+ ),
}))
- const uniqueNetworks = [...new Set(DAOs.map((dao) => dao.network))]
+ // TODO: Update to use new portfolio Runtime API
+ const { data, isLoading: isPortfoliosLoading } = useQuery(['daoPortfolios', daos.map((dao) => dao.address)], () =>
+ Promise.all(daos.map((dao) => firstValueFrom(cent.pools.getBalances([dao.address]))))
+ )
+
+ const { data: subData, isLoading: isSubqueryLoading } = useSubquery(
+ `query ($accounts: [String!]) {
+ accounts(
+ filter: {id: {in: $accounts}}
+ ) {
+ nodes {
+ id
+ investorTransactions {
+ nodes {
+ timestamp
+ tokenPrice
+ tranche {
+ tokenPrice
+ trancheId
+ }
+ }
+ }
+ }
+ }
+ }`,
+ {
+ accounts: daos.map((dao) => dao.address),
+ }
+ )
+
+ const mapped: Row[] = daos.map((dao, i) => {
+ const investTxs = subData?.accounts.nodes.find((n: any) => n.id === dao.address)?.investorTransactions.nodes
+ const trancheBalances =
+ data?.[i].tranches && Object.fromEntries(data[i].tranches.map((t) => [t.trancheId, t.balance.toFloat()]))
+ const yields =
+ trancheBalances &&
+ Object.keys(trancheBalances).map((tid) => {
+ const firstTx = investTxs?.find((tx: any) => tx.tranche.trancheId === tid)
+ const initialTokenPrice = firstTx && new Price(firstTx.tokenPrice).toFloat()
+ const tokenPrice = firstTx && new Price(firstTx.tranche.tokenPrice).toFloat()
+ const profit = tokenPrice / initialTokenPrice - 1
+ return [tid, profit] as const
+ })
+ const totalValue = trancheBalances && Object.values(trancheBalances)?.reduce((acc, balance) => acc + balance, 0)
+ const weightedYield =
+ yields &&
+ totalValue &&
+ yields.reduce((acc, [tid, profit]) => acc + profit * trancheBalances![tid], 0) / totalValue
+
+ return {
+ ...dao,
+ value: totalValue ?? 0,
+ profit: weightedYield ? weightedYield * 100 : 0,
+ networkName: getNetworkName(dao.network),
+ firstInvestment: investTxs?.[0] && new Date(investTxs[0].timestamp),
+ }
+ })
+
+ const uniqueNetworks = [...new Set(daos.map((dao) => dao.network))]
const filters = useFilters({ data: mapped })
- console.log('filters.data', filters.data)
const columns: Column[] = [
{
@@ -124,15 +170,29 @@ function DaoPortfoliosTable() {
{
header: ,
cell: (row: Row) => (
- {row.value && formatBalance(row.value, 'USD')}
+
+ {row.value != null && formatBalance(row.value, 'USD')}
+
),
flex: '3',
sortKey: 'value',
},
{
- header: 'Profit',
+ header: ,
+ cell: (row: Row) => (
+
+ {row.profit != null && formatPercentage(row.profit)}
+
+ ),
+ flex: '3',
+ sortKey: 'profit',
+ },
+ {
+ header: 'First investment',
cell: (row: Row) => (
- {row.value && formatPercentage(row.value)}
+
+ {row.firstInvestment ? formatDate(row.firstInvestment) : '-'}
+
),
flex: '3',
},
@@ -140,7 +200,13 @@ function DaoPortfoliosTable() {
return (
- `/prime/${row.slug}`} />
+ `/prime/${row.slug}`}
+ />
)
}
diff --git a/centrifuge-app/src/utils/useFilters.ts b/centrifuge-app/src/utils/useFilters.ts
index cce76c3565..5d2c23f127 100644
--- a/centrifuge-app/src/utils/useFilters.ts
+++ b/centrifuge-app/src/utils/useFilters.ts
@@ -13,6 +13,17 @@ export function useFilters({ key: prefix = 'f_', data = [], useSearchParams =
const history = useHistory()
const { search } = useLocation()
const [params, setParams] = React.useState(() => new URLSearchParams(useSearchParams ? search : undefined))
+ const state: Record> = {}
+
+ for (const [prefixedKey, value] of params.entries()) {
+ if (!prefixedKey.startsWith(prefix)) continue
+ const key = prefixedKey.slice(prefix.length)
+ if (state[key]) {
+ state[key].add(value)
+ } else {
+ state[key] = new Set([value])
+ }
+ }
const setFilter = useEventCallback((key: string, value: string[]) => {
setParams((prev) => {
@@ -29,31 +40,19 @@ export function useFilters({ key: prefix = 'f_', data = [], useSearchParams =
})
})
- React.useEffect(() => {
- history.replace({ search: params.toString() })
- }, [params, history])
-
- const state: Record> = {}
-
const hasFilter = useEventCallback((key: string, value: string) => {
return !!state[key]?.has(String(value))
})
const getState = useGetLatest(state)
- for (const [prefixedKey, value] of params.entries()) {
- if (!prefixedKey.startsWith(prefix)) continue
- const key = prefixedKey.slice(prefix.length)
- if (state[key]) {
- state[key].add(value)
- } else {
- state[key] = new Set([value])
- }
- }
-
const entries = Object.entries(state)
const filtered = data.filter((entry) => entries.every(([key, set]) => set.has(String(get(entry, key)))))
+ React.useEffect(() => {
+ history.replace({ search: params.toString() })
+ }, [params, history])
+
return {
setFilter,
hasFilter,
diff --git a/centrifuge-app/src/utils/useSubquery.ts b/centrifuge-app/src/utils/useSubquery.ts
new file mode 100644
index 0000000000..ed5eaf5fb5
--- /dev/null
+++ b/centrifuge-app/src/utils/useSubquery.ts
@@ -0,0 +1,10 @@
+import { useCentrifuge } from '@centrifuge/centrifuge-react'
+import { useQuery } from 'react-query'
+import { firstValueFrom } from 'rxjs'
+
+export function useSubquery(query: string, variables?: object) {
+ const cent = useCentrifuge()
+ return useQuery(['subquery', query, variables], () =>
+ firstValueFrom(cent.getSubqueryObservable(query, variables, false))
+ )
+}
From d5a3fb8e692b9c2d0a1720392709fb8ed3245799 Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Tue, 24 Oct 2023 13:15:54 +0200
Subject: [PATCH 5/9] remove logs
---
centrifuge-app/src/components/DataTable.tsx | 4 +---
centrifuge-app/src/pages/IssuerCreatePool/index.tsx | 1 -
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx
index f691622456..5cb0169ede 100644
--- a/centrifuge-app/src/components/DataTable.tsx
+++ b/centrifuge-app/src/components/DataTable.tsx
@@ -297,7 +297,6 @@ export function FilterableTableHeader({
if (!form.current) return
const formData = new FormData(form.current)
const entries = formData.getAll(key) as string[]
- console.log('entries', entries)
filters.setFilter(key, entries)
}
@@ -310,7 +309,6 @@ export function FilterableTableHeader({
}
const state = filters.getState()
const selectedOptions = state[key] as Set | undefined
- console.log('state', state)
return (
@@ -346,7 +344,7 @@ export function FilterableTableHeader({
{optionKeys.map((option, index) => {
const label = Array.isArray(options) ? option : options[option]
const checked = filters.hasFilter(key, option)
- console.log('checked', filters.hasFilter(key, option))
+
return (
{
const [transferToMultisig, aoProxy, adminProxy, , , , , , { adminMultisig }] = args
const multisigAddr = adminMultisig && createKeyMulti(adminMultisig.signers, adminMultisig.threshold)
- console.log('adminMultisig', multisigAddr)
const poolArgs = args.slice(2) as any
return combineLatest([
cent.getApi(),
From 78f5691931b65af5b831d5e80b41a3e46293de3e Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Thu, 26 Oct 2023 12:00:38 +0200
Subject: [PATCH 6/9] fix table
---
centrifuge-app/src/components/DataTable.tsx | 9 ++++-----
centrifuge-app/src/components/styles.ts | 4 ++--
centrifuge-app/src/pages/Prime/index.tsx | 7 ++-----
3 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx
index a4310b8c52..3584ce2041 100644
--- a/centrifuge-app/src/components/DataTable.tsx
+++ b/centrifuge-app/src/components/DataTable.tsx
@@ -20,7 +20,8 @@ import { Link, LinkProps } from 'react-router-dom'
import styled from 'styled-components'
import { useElementScrollSize } from '../utils/useElementScrollSize'
import { FiltersState } from '../utils/useFilters'
-import { FilterButton, QuickAction } from './PoolFilter/styles'
+import { FilterButton } from './FilterButton'
+import { QuickAction } from './QuickAction'
type GroupedProps = {
groupIndex?: number
@@ -275,14 +276,12 @@ export function FilterableTableHeader({
label,
options,
filters,
- align,
tooltip,
}: {
filterKey: string
label: string
options: string[] | Record
filters: FiltersState
- align?: Column['align']
tooltip?: string
}) {
const optionKeys = Array.isArray(options) ? options : Object.keys(options)
@@ -316,13 +315,13 @@ export function FilterableTableHeader({
{label}
-
+
) : (
{label}
-
+
)}
diff --git a/centrifuge-app/src/components/styles.ts b/centrifuge-app/src/components/styles.ts
index 2203668ef8..7617c1d354 100644
--- a/centrifuge-app/src/components/styles.ts
+++ b/centrifuge-app/src/components/styles.ts
@@ -7,8 +7,8 @@ export const buttonActionStyles = css`
background-color: transparent;
border-radius: ${({ theme }) => theme.radii.tooltip}px;
+ &:hover,
&:focus-visible {
- outline: ${({ theme }) => `2px solid ${theme.colors.textSelected}`};
- outline-offset: 4px;
+ color: ${({ theme }) => theme.colors.textInteractiveHover};
}
`
diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx
index 96be8fe4f7..3a9672712b 100644
--- a/centrifuge-app/src/pages/Prime/index.tsx
+++ b/centrifuge-app/src/pages/Prime/index.tsx
@@ -152,7 +152,6 @@ function DaoPortfoliosTable() {
{row.name}
),
- flex: '1',
},
{
align: 'left',
@@ -165,7 +164,6 @@ function DaoPortfoliosTable() {
/>
),
cell: (row: Row) => {row.networkName},
- flex: '3',
},
{
header: ,
@@ -174,7 +172,6 @@ function DaoPortfoliosTable() {
{row.value != null && formatBalance(row.value, 'USD')}
),
- flex: '3',
sortKey: 'value',
},
{
@@ -184,17 +181,17 @@ function DaoPortfoliosTable() {
{row.profit != null && formatPercentage(row.profit)}
),
- flex: '3',
sortKey: 'profit',
},
{
+ align: 'left',
header: 'First investment',
cell: (row: Row) => (
{row.firstInvestment ? formatDate(row.firstInvestment) : '-'}
),
- flex: '3',
+ width: '5fr',
},
]
From 9416804cfc0b3381c8483540fa2e3214fc4d25ae Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Fri, 27 Oct 2023 10:20:08 +0200
Subject: [PATCH 7/9] fix color
---
centrifuge-app/src/components/DataTable.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/centrifuge-app/src/components/DataTable.tsx b/centrifuge-app/src/components/DataTable.tsx
index 3584ce2041..caa3f9a0dd 100644
--- a/centrifuge-app/src/components/DataTable.tsx
+++ b/centrifuge-app/src/components/DataTable.tsx
@@ -374,7 +374,7 @@ export function FilterableTableHeader({
}
const StyledHeader = styled(Shelf)`
- color: ${({ theme }) => theme.colors.textSecondary};
+ color: ${({ theme }) => theme.colors.textPrimary};
cursor: pointer;
appearance: none;
border: none;
From 888c924ec3265d04a10da37e52b9de3431bc2205 Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Mon, 30 Oct 2023 10:21:10 +0100
Subject: [PATCH 8/9] show prime
---
centrifuge-app/src/components/Menu/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/centrifuge-app/src/components/Menu/index.tsx b/centrifuge-app/src/components/Menu/index.tsx
index 55166bc069..63770079ce 100644
--- a/centrifuge-app/src/components/Menu/index.tsx
+++ b/centrifuge-app/src/components/Menu/index.tsx
@@ -54,7 +54,7 @@ export function Menu() {
Portfolio
)}
- {showPrime && address && (
+ {showPrime && (
Prime
From 5396d045760d66c43c63c56b25d4ed8c03e5cda3 Mon Sep 17 00:00:00 2001
From: Onno Visser <23527729+onnovisser@users.noreply.github.com>
Date: Mon, 30 Oct 2023 10:27:46 +0100
Subject: [PATCH 9/9] fix export
---
centrifuge-app/src/pages/Prime/Detail.tsx | 2 +-
centrifuge-app/src/pages/Prime/index.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/centrifuge-app/src/pages/Prime/Detail.tsx b/centrifuge-app/src/pages/Prime/Detail.tsx
index 186f823fad..670153f93d 100644
--- a/centrifuge-app/src/pages/Prime/Detail.tsx
+++ b/centrifuge-app/src/pages/Prime/Detail.tsx
@@ -1,7 +1,7 @@
import { useParams } from 'react-router'
import { LayoutBase } from '../../components/LayoutBase'
-export function PrimeDetailPage() {
+export default function PrimeDetailPage() {
return (
diff --git a/centrifuge-app/src/pages/Prime/index.tsx b/centrifuge-app/src/pages/Prime/index.tsx
index 3a9672712b..0dd310a991 100644
--- a/centrifuge-app/src/pages/Prime/index.tsx
+++ b/centrifuge-app/src/pages/Prime/index.tsx
@@ -30,7 +30,7 @@ const DAOs: DAO[] = [
},
]
-export function PrimePage() {
+export default function PrimePage() {
return (