Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4 feature: filter omnichain activity list results #4579

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,16 @@ DB_PROJECTS_WEBHOOK_URL=
CONTACT_WEBHOOK_URL=


NEXT_PUBLIC_V4_ENABLED=false
NEXT_PUBLIC_V4_ENABLED=false

# Server Subgraph URLs for V4
V4_SEPOLIA_SUBGRAPH_URL=
V4_OPTIMISM_SEPOLIA_SUBGRAPH_URL=
V4_BASE_SEPOLIA_SUBGRAPH_URL=
V4_ARBITRUM_SEPOLIA_SUBGRAPH_URL=

# Public Subgraph URLs for V4
NEXT_PUBLIC_V4_SEPOLIA_SUBGRAPH_URL=
NEXT_PUBLIC_V4_OPTIMISM_SEPOLIA_SUBGRAPH_URL=
NEXT_PUBLIC_V4_BASE_SEPOLIA_SUBGRAPH_URL=
NEXT_PUBLIC_V4_ARBITRUM_SEPOLIA_SUBGRAPH_URL=
63 changes: 63 additions & 0 deletions src/components/icons/Arbitrum.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export function ArbitrumLogoIcon({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 2500 2500"
className={className}
>
<g id="Layer_x0020_1">
<g id="_2405588477232">
<rect className="fill-none" width="2500" height="2500"></rect>
<g>
<g>
<path
className="fill-[#213147]"
d="M226,760v980c0,63,33,120,88,152l849,490c54,31,121,31,175,0l849-490c54-31,88-89,88-152V760 c0-63-33-120-88-152l-849-490c-54-31-121-31-175,0L314,608c-54,31-87,89-87,152H226z"
></path>
<g>
<g>
<g>
<path
className="fill-[#12aaff]"
d="M1435,1440l-121,332c-3,9-3,19,0,29l208,571l241-139l-289-793C1467,1422,1442,1422,1435,1440z"
></path>
</g>
<g>
<path
className="fill-[#12aaff]"
d="M1678,882c-7-18-32-18-39,0l-121,332c-3,9-3,19,0,29l341,935l241-139L1678,883V882z"
></path>
</g>
</g>
</g>
<g>
<path
className="fill-[#9dcced]"
d="M1250,155c6,0,12,2,17,5l918,530c11,6,17,18,17,30v1060c0,12-7,24-17,30l-918,530c-5,3-11,5-17,5 s-12-2-17-5l-918-530c-11-6-17-18-17-30V719c0-12,7-24,17-30l918-530c5-3,11-5,17-5l0,0V155z M1250,0c-33,0-65,8-95,25L237,555 c-59,34-95,96-95,164v1060c0,68,36,130,95,164l918,530c29,17,62,25,95,25s65-8,95-25l918-530c59-34,95-96,95-164V719 c0-68-36-130-95-164L1344,25c-29-17-62-25-95-25l0,0H1250z"
></path>
</g>
<polygon
className="fill-[#213147]"
points="642,2179 727,1947 897,2088 738,2234 "
></polygon>
<g>
<path
className="fill-white"
d="M1172,644H939c-17,0-33,11-39,27L401,2039l241,139l550-1507c5-14-5-28-19-28L1172,644z"
></path>
<path
className="fill-white"
d="M1580,644h-233c-17,0-33,11-39,27L738,2233l241,139l620-1701c5-14-5-28-19-28V644z"
></path>
</g>
</g>
</g>
</g>
</g>
</svg>
)
}
15 changes: 15 additions & 0 deletions src/components/icons/Base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function BaseLogoIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 111 111"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M54.921 110.034C85.359 110.034 110.034 85.402 110.034 55.017C110.034 24.6319 85.359 0 54.921 0C26.0432 0 2.35281 22.1714 0 50.3923H72.8467V59.6416H3.9565e-07C2.35281 87.8625 26.0432 110.034 54.921 110.034Z"
fill="#0052FF"
/>
</svg>
)
}
32 changes: 32 additions & 0 deletions src/components/icons/Optimism.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export function OptimismLogoIcon({ className }: { className?: string }) {
return (
<svg
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
viewBox="0 0 500 500"
className={className}
>
<circle className="fill-[#FF0420]" cx="250" cy="250" r="250" />
<path
className="fill-white"
d="M177.1,316.4c-14.9,0-27.1-3.5-36.6-10.5c-9.4-7.1-14.1-17.3-14.1-30.4c0-2.8,0.3-6.1,0.9-10.1
c1.6-9,3.9-19.8,6.9-32.5c8.5-34.4,30.5-51.6,65.9-51.6c9.6,0,18.3,1.6,25.9,4.9c7.6,3.1,13.6,7.9,18,14.3
c4.4,6.3,6.6,13.8,6.6,22.5c0,2.6-0.3,5.9-0.9,9.9c-1.9,11.1-4.1,22-6.8,32.5c-4.4,17.1-11.9,30-22.7,38.5
C209.5,312.3,195.1,316.4,177.1,316.4z M179.8,289.4c7,0,12.9-2.1,17.8-6.2c5-4.1,8.6-10.4,10.7-19c2.9-11.8,5.1-22,6.6-30.8
c0.5-2.6,0.8-5.3,0.8-8.1c0-11.4-5.9-17.1-17.8-17.1c-7,0-13,2.1-18,6.2c-4.9,4.1-8.4,10.4-10.5,19c-2.3,8.4-4.5,18.6-6.8,30.8
c-0.5,2.5-0.8,5.1-0.8,7.9C161.7,283.7,167.8,289.4,179.8,289.4z"
/>
<path
className="fill-white"
d="M259.3,314.6c-1.4,0-2.4-0.4-3.2-1.3c-0.6-1-0.8-2.1-0.6-3.4l25.9-122c0.2-1.4,0.9-2.5,2.1-3.4
c1.1-0.9,2.3-1.3,3.6-1.3H337c13.9,0,25,2.9,33.4,8.6c8.5,5.8,12.8,14.1,12.8,25c0,3.1-0.4,6.4-1.1,9.8c-3.1,14.4-9.4,25-19,31.9
c-9.4,6.9-22.3,10.3-38.7,10.3h-25.3l-8.6,41.1c-0.3,1.4-0.9,2.5-2.1,3.4c-1.1,0.9-2.3,1.3-3.6,1.3H259.3z M325.7,242.9
c5.3,0,9.8-1.4,13.7-4.3c4-2.9,6.6-7,7.9-12.4c0.4-2.1,0.6-4,0.6-5.6c0-3.6-1.1-6.4-3.2-8.3c-2.1-2-5.8-3-10.9-3h-22.5l-7.1,33.6
H325.7z"
/>
</svg>
)
}
25 changes: 24 additions & 1 deletion src/lib/apollo/subgraphUri.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { isBrowser } from 'utils/isBrowser'
import { sepolia } from 'viem/chains'
import {
arbitrumSepolia,
baseSepolia,
optimism,
optimismSepolia,
sepolia,
} from 'viem/chains'

export const subgraphUri = () => {
let uri: string | undefined
Expand Down Expand Up @@ -29,10 +35,27 @@ export const v4SubgraphUri = (chainId: number) => {
let uri: string | undefined

const env = {
[optimism.id]: {
browserUrl: process.env.NEXT_PUBLIC_V4_OPTIMISM_SUBGRAPH_URL,
serverUrl: process.env.V4_OPTIMISM_SUBGRAPH_URL,
},
// Test nets
[sepolia.id]: {
browserUrl: process.env.NEXT_PUBLIC_V4_SEPOLIA_SUBGRAPH_URL,
serverUrl: process.env.V4_SEPOLIA_SUBGRAPH_URL,
},
[optimismSepolia.id]: {
browserUrl: process.env.NEXT_PUBLIC_V4_OPTIMISM_SEPOLIA_SUBGRAPH_URL,
serverUrl: process.env.V4_OPTIMISM_SEPOLIA_SUBGRAPH_URL,
},
[baseSepolia.id]: {
browserUrl: process.env.NEXT_PUBLIC_V4_BASE_SEPOLIA_SUBGRAPH_URL,
serverUrl: process.env.V4_BASE_SEPOLIA_SUBGRAPH_URL,
},
[arbitrumSepolia.id]: {
browserUrl: process.env.NEXT_PUBLIC_V4_ARBITRUM_SEPOLIA_SUBGRAPH_URL,
serverUrl: process.env.V4_ARBITRUM_SEPOLIA_SUBGRAPH_URL,
},
} as Record<number, { browserUrl?: string; serverUrl?: string }>

if (isBrowser()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { Button } from 'antd'
import { JuiceListbox } from 'components/inputs/JuiceListbox'
import Loading from 'components/Loading'
import RichNote from 'components/RichNote/RichNote'
import { NETWORKS } from 'constants/networks'
import request from 'graphql-request'
import {
NativeTokenValue,
useJBChainId,
useJBContractContext,
} from 'juice-sdk-react'
import { JBChainId, SuckerPair } from 'juice-sdk-core'
import { NativeTokenValue, useJBChainId, useSuckers } from 'juice-sdk-react'
import { v4SubgraphUri } from 'lib/apollo/subgraphUri'
import { last } from 'lodash'
import { useProjectContext } from 'packages/v2v3/components/V2V3Project/ProjectDashboard/hooks/useProjectContext'
Expand All @@ -29,50 +27,26 @@ import {
const PAGE_SIZE = 10

export function V4ActivityList() {
const { projectId } = useJBContractContext()
const tokenSymbol = useProjectContext().tokenSymbol
const chainId = useJBChainId()
const { data: suckers, isLoading: suckersLoading } = useSuckers()

const [selectedChainId, setSelectedChainId] = React.useState(chainId)
const [filter, setFilter] = React.useState<ProjectEventFilter>('all')

const chainId = useJBChainId()
const supportedChains = React.useMemo(
() =>
CHAIN_OPTIONS.filter(o => suckers?.find(s => s.peerChainId === o.value)),
[suckers],
)

const {
data: projectEventsQueryResult,
isLoading,
fetchNextPage,
} = useInfiniteQuery({
queryKey: ['projectEvents', projectId, filter],
initialPageParam: 0,
queryFn: async ({ pageParam = 0 }) => {
if (!chainId) {
throw new Error('useSubgraphQuery needs a chainId, none provided')
}
const uri = v4SubgraphUri(chainId)
const document = ProjectEventsDocument

const data = await request(uri, document, {
orderBy: ProjectEvent_OrderBy.timestamp,
orderDirection: OrderDirection.desc,
where: {
projectId: Number(projectId),
// ProjectEvents have exactly one non-null Event field. We can use `<filter>_not: null` to return only projectEvents where the matching Event field is defined
...(!filter || filter === 'all'
? {}
: {
[filter + '_not']: null,
}),
},
skip: pageParam * PAGE_SIZE,
first: PAGE_SIZE,
})
const mightHaveNextPage = data.projectEvents.length === PAGE_SIZE
return {
data,
nextCursor: mightHaveNextPage ? pageParam + PAGE_SIZE : undefined,
}
},
getNextPageParam: lastPage => {
return lastPage.nextCursor
},
} = useOmnichainSubgraphProjectQuery({
sucker: suckers?.find(s => s.peerChainId === selectedChainId),
filter,
})

const projectEvents = React.useMemo(
Expand All @@ -87,8 +61,14 @@ export function V4ActivityList() {

return (
<div>
<div className="flex items-baseline justify-between">
<div className="flex items-baseline justify-between gap-5">
<h2 className="mb-6 font-heading text-2xl font-medium">Activity</h2>
<JuiceListbox
className="w-full min-w-0 max-w-[224px]"
value={CHAIN_OPTIONS.find(o => o.value === selectedChainId)}
options={supportedChains}
onChange={o => setSelectedChainId(o.value as JBChainId)}
/>
</div>
<div className="flex flex-col gap-3">
<JuiceListbox
Expand All @@ -97,8 +77,10 @@ export function V4ActivityList() {
value={ACTIVITY_OPTIONS.find(o => o.value === filter)}
onChange={o => setFilter(o.value as ProjectEventFilter)}
/>
{isLoading && <Loading />}
{isLoading || (projectEvents && projectEvents.length > 0) ? (
{(isLoading || suckersLoading) && <Loading />}
{isLoading ||
suckersLoading ||
(projectEvents && projectEvents.length > 0) ? (
<>
{projectEvents?.map(event => {
return (
Expand Down Expand Up @@ -312,3 +294,96 @@ const ACTIVITY_OPTIONS = [
{ label: 'Used allowance', value: 'useAllowanceEvent' },
{ label: 'Burned', value: 'burnEvent' },
]

const useOmnichainSubgraphProjectQuery = ({
filter,
sucker,
}: {
filter?: ProjectEventFilter
sucker: SuckerPair | undefined
}) => {
const result = useInfiniteQuery({
queryKey: ['projectEvents', sucker?.projectId, sucker?.peerChainId, filter],
initialPageParam: 0,
queryFn: async ({ pageParam = 0 }) => {
if (!sucker) return { data: { projectEvents: [] }, nextCursor: undefined }
const uri = v4SubgraphUri(sucker.peerChainId)
const document = ProjectEventsDocument

const data = await request(uri, document, {
orderBy: ProjectEvent_OrderBy.timestamp,
orderDirection: OrderDirection.desc,
where: {
projectId: Number(sucker.projectId),
// ProjectEvents have exactly one non-null Event field. We can use `<filter>_not: null` to return only projectEvents where the matching Event field is defined
...(!filter || filter === 'all'
? {}
: {
[filter + '_not']: null,
}),
},
skip: pageParam * PAGE_SIZE,
first: PAGE_SIZE,
})
const mightHaveNextPage = data.projectEvents.length === PAGE_SIZE
return {
data,
nextCursor: mightHaveNextPage ? pageParam + PAGE_SIZE : undefined,
}
},
getNextPageParam: lastPage => {
return lastPage.nextCursor
},
})

return result
}

const CHAIN_OPTIONS = Object.entries(NETWORKS).map(
([chainId, networkInfo]) => ({
label: networkInfo.label,
value: parseInt(chainId),
}),
)

// TODO: Chain options as icons
// const Chain: React.FC<PropsWithChildren> = ({ children }) => {
// return (
// <div className="flex h-6 w-6 items-center justify-center">{children}</div>
// )
// }

// const CHAIN_OPTIONS = [
// {
// label: (
// <Chain>
// <EthereumLogo />
// </Chain>
// ),
// value: 'ethereum' as const,
// },
// {
// label: (
// <Chain>
// <OptimismLogoIcon />
// </Chain>
// ),
// value: 'optimism' as const,
// },
// {
// label: (
// <Chain>
// <ArbitrumLogoIcon />
// </Chain>
// ),
// value: 'arbitrum' as const,
// },
// {
// label: (
// <Chain>
// <BaseLogoIcon />
// </Chain>
// ),
// value: 'base' as const,
// },
// ]
Loading