Skip to content
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.4.9/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
Expand Down
2 changes: 2 additions & 0 deletions src/app/service-providers/components/DesktopTableFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CapacityFilter } from './CapacityFilter'
import { IpniFilter } from './IpniFilter'
import { LocationFilter } from './LocationFilter'
import { ProvingPeriodFilter } from './ProvingPeriodFilter'
import { ReachableFilter } from './ReachableFilter'
import { ServiceTierFilter } from './ServiceTierFilter'
import type { useFilterOptions } from '../hooks/use-filter-options'
import { useFilterQueryState } from '../hooks/use-filter-query-state'
Expand Down Expand Up @@ -72,6 +73,7 @@ export function DesktopTableFilters({ options }: DesktopTableFiltersProps) {
provingPeriodMin={provingPeriodMin}
provingPeriodMax={provingPeriodMax}
/>
<ReachableFilter />
{ipniOptions.length > 1 && <IpniFilter options={ipniOptions} />}
{serviceTierOptions.length > 1 && (
<ServiceTierFilter options={serviceTierOptions} />
Expand Down
2 changes: 2 additions & 0 deletions src/app/service-providers/components/MobileTableFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { CapacityFilter } from './CapacityFilter'
import { IpniFilter } from './IpniFilter'
import { LocationFilter } from './LocationFilter'
import { ProvingPeriodFilter } from './ProvingPeriodFilter'
import { ReachableFilter } from './ReachableFilter'
import { ServiceTierFilter } from './ServiceTierFilter'
import type { useFilterOptions } from '../hooks/use-filter-options'
import { useFilterQueryState } from '../hooks/use-filter-query-state'
Expand Down Expand Up @@ -74,6 +75,7 @@ export function MobileTableFilters({ options }: MobileTableFiltersProps) {
provingPeriodMin={provingPeriodMin}
provingPeriodMax={provingPeriodMax}
/>
<ReachableFilter />

{ipniOptions.length > 1 && <IpniFilter options={ipniOptions} />}

Expand Down
32 changes: 32 additions & 0 deletions src/app/service-providers/components/ReachableFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Fieldset } from '@headlessui/react'

import { CheckboxesContainer } from '@/components/CheckboxesContainer'
import { CheckboxWithLabel } from '@/components/CheckboxWithLabel'
import { FilterHeading } from '@/components/FilterHeading'

import { useFilterQueryState } from '../hooks/use-filter-query-state'

const REACHABLE_FILTER_OPTIONS = [
{ value: 'true', label: 'Accessible' },
{ value: 'false', label: 'Unavailable' },
] as const

export function ReachableFilter() {
const { filterQueries, toggleFilterQuery } = useFilterQueryState()

return (
<Fieldset>
<FilterHeading>Node Accessibility</FilterHeading>
<CheckboxesContainer>
{REACHABLE_FILTER_OPTIONS.map((option) => (
<CheckboxWithLabel
key={option.value}
checked={filterQueries.reachable.includes(option.value)}
onChange={() => toggleFilterQuery('reachable', option.value)}
label={option.label}
/>
))}
</CheckboxesContainer>
</Fieldset>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export function ServiceProvidersTable({ data }: ServiceProvidersTableProps) {
</div>
</div>

<p className="mb-3 text-xs text-(--color-text-muted)">
Legend: <span aria-hidden>🏅</span> Endorsed = Endorsed (EndorsementSet)
| Warm storage = Approved (FWSS)
</p>

{hasSearchResults ? (
<TanstackTable table={table} maxHeight="100vh" />
) : (
Expand Down
3 changes: 3 additions & 0 deletions src/app/service-providers/data/column-definition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ipniFilterFn,
locationFilterFn,
provingPeriodRangeFilterFn,
reachableFilterFn,
serviceTierFilterFn,
} from '../utils/service-provider-filters'

Expand All @@ -40,6 +41,7 @@ export const columns = [
description={row.description}
address={row.serviceProviderAddress}
serviceUrl={row.serviceUrl}
isEndorsed={row.isEndorsed}
/>
)
},
Expand All @@ -53,6 +55,7 @@ export const columns = [
},
sortingFn: sortSoftwareVersion,
sortUndefined: 'last',
filterFn: reachableFilterFn,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reachable filter is attached to the softwareVersion column, which means this column now carries two concerns (displaying version + filtering reachability). If another filter is ever applied to this column, they'd conflict. Worth considering a dedicated virtual/computed column for reachability to keep things clean and make the relationship explicit.

}),
columnHelper.accessor('isActive', {
id: 'serviceOffered',
Expand Down
13 changes: 12 additions & 1 deletion src/app/service-providers/hooks/use-filter-query-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import type { ServiceTier } from '@/utils/service-tier'
import { toggleValueInArray } from '@/utils/toggle-value-in-array'

import { parseNumericInput } from '../utils/parse-numeric-input'
import { parseAsReachableFilterValue } from '../utils/parse-reachable-filter-value'
import { parseAsServiceTier } from '../utils/parse-service-tier'

export type ReachableFilterValue = 'true' | 'false'

export type FilterState = {
location: Array<string>
capacityMin: number | null
Expand All @@ -20,8 +23,11 @@ export type FilterState = {
provingPeriodMax: number | null
ipni: Array<string>
serviceTier: Array<ServiceTier>
reachable: Array<ReachableFilterValue>
}

const DEFAULT_REACHABLE_FILTER: Array<ReachableFilterValue> = ['true']

const filterParsers = {
location: parseAsArrayOf(parseAsString).withDefault([]),
capacityMin: parseAsInteger,
Expand All @@ -30,6 +36,9 @@ const filterParsers = {
provingPeriodMax: parseAsInteger,
ipni: parseAsArrayOf(parseAsString).withDefault([]),
serviceTier: parseAsArrayOf(parseAsServiceTier).withDefault([]),
reachable: parseAsArrayOf(parseAsReachableFilterValue).withDefault(
DEFAULT_REACHABLE_FILTER,
),
}

type ArrayKeys<T> = {
Expand Down Expand Up @@ -77,7 +86,9 @@ export function useFilterQueryState() {

const activeFilterCount = useMemo(() => {
return Object.entries(filterQueries).reduce((count, [_, value]) => {
if (Array.isArray(value)) return count + (value.length > 0 ? 1 : 0)
if (Array.isArray(value)) {
return count + (value.length > 0 ? 1 : 0)
}
if (value != null) return count + 1
return count
}, 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function mapFilterStateToColumnFilters({
provingPeriodMax,
ipni,
serviceTier,
reachable,
}: FilterState) {
const columnFilters: ServiveProviderColumnFilters = []

Expand Down Expand Up @@ -49,6 +50,9 @@ export function mapFilterStateToColumnFilters({
if (serviceTier.length > 0) {
columnFilters.push({ id: 'serviceOffered', value: serviceTier })
}
if (reachable.length > 0) {
columnFilters.push({ id: 'softwareVersion', value: reachable })
}

return columnFilters
}
13 changes: 13 additions & 0 deletions src/app/service-providers/utils/parse-reachable-filter-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createParser } from 'nuqs'

import type { ReachableFilterValue } from '../hooks/use-filter-query-state'

export const parseAsReachableFilterValue = createParser({
parse: (value) => {
if (value === 'true' || value === 'false') {
return value as ReachableFilterValue
}
return null
},
serialize: (value) => value,
})
19 changes: 19 additions & 0 deletions src/app/service-providers/utils/service-provider-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ export const ipniFilterFn: FilterFn<ServiceProvider> = (
return ipniArray.includes(ipniValue)
}

export const reachableFilterFn: FilterFn<ServiceProvider> = (
row,
_columnId,
filterValue,
) => {
const reachableArray = filterValue as FilterState['reachable']
if (reachableArray.length === 0) return true

// Reachability proxy: Curio only populates softwareVersion when the node is reachable.
// Treat any defined value (including empty string) as reachable.
const hasSoftwareVersion =
row.original.softwareVersion !== null &&
row.original.softwareVersion !== undefined
const reachableValue: FilterState['reachable'][number] = hasSoftwareVersion
? 'true'
: 'false'
return reachableArray.includes(reachableValue)
}

export const capacityRangeFilterFn: FilterFn<ServiceProvider> = (
row,
_columnId,
Expand Down
10 changes: 8 additions & 2 deletions src/app/warm-storage-service/data/column-definition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@ export const columns = [
id: 'provider',
header: 'Provider',
cell: (info) => {
const { name, description, serviceProviderAddress, serviceUrl } =
info.row.original
const {
name,
description,
serviceProviderAddress,
serviceUrl,
isEndorsed,
} = info.row.original

return (
<ProviderOverview
name={name}
description={description}
address={serviceProviderAddress}
serviceUrl={serviceUrl}
isEndorsed={isEndorsed}
/>
)
},
Expand Down
15 changes: 14 additions & 1 deletion src/components/ProviderOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ type ProviderOverviewProps = {
description: string
address: string
serviceUrl: string
isEndorsed: boolean
}

export function ProviderOverview({
name,
description,
address,
serviceUrl,
isEndorsed,
}: ProviderOverviewProps) {
const [network] = useNetwork()
const networkId = getNetworkId(network)
Expand All @@ -32,7 +34,18 @@ export function ProviderOverview({
href={`${explorerUrl}${address}`}
aria-label={`View provider ${name} on PDP Scan`}
>
<span className="font-medium">{name}</span>
<span className="font-medium inline-flex items-center gap-1">
<span>{name}</span>
{isEndorsed && (
<span
role="img"
aria-label="Endorsed Provider"
title="Endorsed Provider"
>
🏅
</span>
)}
</span>
</ExternalTextLink>

<p className="truncate pt-1 text-sm text-gray-600">{description}</p>
Expand Down
5 changes: 5 additions & 0 deletions src/config/abis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const WarmStorageViewABI = parseAbi([
'function isProviderApproved(uint256 providerId) view returns (bool)',
])

export const EndorsementSetABI = parseAbi([
'function getProviderIds() view returns (uint256[])',
'function containsProviderId(uint256 providerId) view returns (bool)',
])

export const ServiceRegistryABI = parseAbi([
'function getProvider(uint256 providerId) view returns ((uint256 providerId, (address serviceProvider, address payee, string name, string description, bool isActive) info))',
'function getProductCapabilities(uint256 providerId, uint8 productType, string[] keys) view returns (bytes[])',
Expand Down
19 changes: 18 additions & 1 deletion src/config/chains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Address, ChainContract, Chain as ViemChain } from 'viem'

import { ServiceRegistryABI, WarmStorageABI, WarmStorageViewABI } from './abis'
import {
EndorsementSetABI,
ServiceRegistryABI,
WarmStorageABI,
WarmStorageViewABI,
} from './abis'
import deployments from './deployments'

export type UpgradableContract = {
Expand Down Expand Up @@ -38,6 +43,10 @@ export interface Chain extends ViemChain {
address: Address
abi: typeof ServiceRegistryABI
}
endorsementSet: {
address: Address
abi: typeof EndorsementSetABI
}
}
contracts: Contracts
linkToRelease?: string
Expand Down Expand Up @@ -94,6 +103,10 @@ const mainnet: Chain = {
address: mainnetAddresses.SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS,
abi: ServiceRegistryABI,
},
endorsementSet: {
address: mainnetAddresses.ENDORSEMENT_SET_ADDRESS,
abi: EndorsementSetABI,
},
},
contracts: {
pdp: {
Expand Down Expand Up @@ -167,6 +180,10 @@ export const calibration: Chain = {
address: calibrationAddresses.SERVICE_PROVIDER_REGISTRY_PROXY_ADDRESS,
abi: ServiceRegistryABI,
},
endorsementSet: {
address: calibrationAddresses.ENDORSEMENT_SET_ADDRESS,
abi: EndorsementSetABI,
},
},
contracts: {
pdp: {
Expand Down
1 change: 1 addition & 0 deletions src/schemas/provider-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const providerSchema = z.object({
ipniPiece: z.boolean(),
isActive: z.boolean(),
isApproved: z.boolean(),
isEndorsed: z.boolean(),
location: z.string(),
maxPieceSize: z.bigint(),
minPieceSize: z.bigint(),
Expand Down
Loading
Loading