Skip to content

Commit 073106b

Browse files
authored
Tables: Source risk category from live DB (#2893)
* dynamic category sourcing: supabase * fix + tests * more table updates * batched feed approach * removed verbose logging * updated key and test * lint fix * env naming * removed tests * feedCategories refactor * remove tests + refactor * tables.tsx cleanup * remove tests * remove feedenhancement * aptos bugfix * lint:fix * remove more tests * supbase fix * restored deprecating tag * fix merge * lint fix * restore paginate noop * nit * reverted unnecessary change * lint fix * remove logging * astro version restore, remove typecheck test * update package.json * remove more logging * cut unused testing functions * update categorziation logic to include hidden field * lint fix * updated sorting
1 parent 155d543 commit 073106b

File tree

8 files changed

+495
-74
lines changed

8 files changed

+495
-74
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ server.log
1717

1818
# environment variables
1919
.env
20+
.env.local
2021
.env.production
2122

2223
# macOS-specific files

package-lock.json

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"@nanostores/preact": "^0.5.2",
6868
"@nanostores/react": "^0.8.4",
6969
"@openzeppelin/contracts": "^4.9.6",
70+
"@supabase/supabase-js": "^2.53.0",
7071
"astro": "^5.13.5",
7172
"@solana-program/compute-budget": "^0.9.0",
7273
"@solana-program/system": "^0.8.0",

src/db/feedCategories.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { supabase } from "./supabase.js"
2+
3+
/* ===========================
4+
Types
5+
=========================== */
6+
7+
type FeedRiskRow = {
8+
proxy_address: string
9+
network: string
10+
risk_status: string | null
11+
}
12+
13+
export type FeedTierResult = { final: string | null }
14+
15+
/* ===========================
16+
Category Config
17+
=========================== */
18+
19+
export const FEED_CATEGORY_CONFIG = {
20+
low: {
21+
key: "low",
22+
name: "Low Market Risk",
23+
icon: "🟢",
24+
title: "Low Market Risk - Feeds that deliver a market price for liquid assets with robust market structure.",
25+
link: "/data-feeds/selecting-data-feeds#-low-market-risk-feeds",
26+
},
27+
medium: {
28+
key: "medium",
29+
name: "Medium Market Risk",
30+
icon: "🟡",
31+
title:
32+
"Medium Market Risk - Feeds that deliver a market price for assets that show signs of liquidity-related risk or other market structure-related risk.",
33+
link: "/data-feeds/selecting-data-feeds#-medium-market-risk-feeds",
34+
},
35+
high: {
36+
key: "high",
37+
name: "High Market Risk",
38+
icon: "🔴",
39+
title:
40+
"High Market Risk - Feeds that deliver a heightened degree of some of the risk factors associated with Medium Market Risk Feeds, or a separate risk that makes the market price subject to uncertainty or volatile. In using a high market risk data feed you acknowledge that you understand the risks associated with such a feed and that you are solely responsible for monitoring and mitigating such risks.",
41+
link: "/data-feeds/selecting-data-feeds#-high-market-risk-feeds",
42+
},
43+
new: {
44+
key: "new",
45+
name: "New Token",
46+
icon: "🟠",
47+
title:
48+
"New Token - Tokens without the historical data required to implement a risk assessment framework may be launched in this category. Users must understand the additional market and volatility risks inherent with such assets. Users of New Token Feeds are responsible for independently verifying the liquidity and stability of the assets priced by feeds that they use.",
49+
link: "/data-feeds/selecting-data-feeds#-new-token-feeds",
50+
},
51+
custom: {
52+
key: "custom",
53+
name: "Custom",
54+
icon: "🔵",
55+
title:
56+
"Custom - Feeds built to serve a specific use case or rely on external contracts or data sources. These might not be suitable for general use or your use case's risk parameters. Users must evaluate the properties of a feed to make sure it aligns with their intended use case.",
57+
link: "/data-feeds/selecting-data-feeds#-custom-feeds",
58+
},
59+
deprecating: {
60+
key: "deprecating",
61+
name: "Deprecating",
62+
icon: "⭕",
63+
title:
64+
"Deprecating - These feeds are scheduled for deprecation. See the [Deprecation](/data-feeds/deprecating-feeds) page to learn more.",
65+
link: "/data-feeds/deprecating-feeds",
66+
},
67+
} as const
68+
69+
export type CategoryKey = keyof typeof FEED_CATEGORY_CONFIG
70+
71+
/* ===========================
72+
Small helpers
73+
=========================== */
74+
75+
const TABLE = "docs_feeds_risk"
76+
77+
const normalizeKey = (v?: string | null): CategoryKey | undefined => {
78+
if (!v) return undefined
79+
const key = v.toLowerCase() as CategoryKey
80+
return key in FEED_CATEGORY_CONFIG ? key : undefined
81+
}
82+
83+
const chooseTier = (dbTier: string | null | undefined, fallback?: string): string | null => dbTier ?? fallback ?? null
84+
85+
const defaultCategoryList = () => Object.values(FEED_CATEGORY_CONFIG).map(({ key, name }) => ({ key, name }))
86+
87+
/* ===========================
88+
Public API
89+
=========================== */
90+
91+
export const getDefaultCategories = defaultCategoryList
92+
93+
/** Merge static categories with those dynamically present in the table. */
94+
export async function getFeedCategories() {
95+
try {
96+
if (!supabase) return defaultCategoryList()
97+
98+
const { data, error } = await supabase
99+
.from(TABLE)
100+
.select("risk_status")
101+
.not("risk_status", "is", null)
102+
.neq("risk_status", "hidden")
103+
104+
if (error || !data) return defaultCategoryList()
105+
106+
const dynamic = Array.from(
107+
new Set(data.map((d) => normalizeKey(d.risk_status)).filter(Boolean) as CategoryKey[])
108+
).map((key) => ({ key, name: FEED_CATEGORY_CONFIG[key].name }))
109+
110+
// Dedup by key while keeping all defaults first
111+
const byKey = new Map<string, { key: string; name: string }>()
112+
defaultCategoryList().forEach((c) => byKey.set(c.key, c))
113+
dynamic.forEach((c) => byKey.set(c.key, c))
114+
115+
return Array.from(byKey.values())
116+
} catch {
117+
return defaultCategoryList()
118+
}
119+
}
120+
121+
/**
122+
* Batch lookup: returns a Map of `${address}-${network}` → { final }.
123+
* Uses DB value when present; otherwise uses per-item fallback.
124+
*/
125+
export async function getFeedRiskTiersBatch(
126+
feedRequests: Array<{
127+
contractAddress: string
128+
network: string
129+
fallbackCategory?: string
130+
}>
131+
): Promise<Map<string, FeedTierResult>> {
132+
const out = new Map<string, FeedTierResult>()
133+
const keyFor = (addr: string, net: string) => `${addr}-${net}`
134+
135+
if (!supabase) {
136+
feedRequests.forEach(({ contractAddress, network, fallbackCategory }) =>
137+
out.set(keyFor(contractAddress, network), { final: chooseTier(null, fallbackCategory) })
138+
)
139+
return out
140+
}
141+
142+
const networks = Array.from(new Set(feedRequests.map((r) => r.network)))
143+
const addresses = Array.from(new Set(feedRequests.map((r) => r.contractAddress)))
144+
145+
try {
146+
const { data, error } = await supabase
147+
.from(TABLE)
148+
.select("proxy_address, network, risk_status")
149+
.in("proxy_address", addresses)
150+
.in("network", networks)
151+
.limit(1000)
152+
153+
if (error) {
154+
feedRequests.forEach(({ contractAddress, network, fallbackCategory }) =>
155+
out.set(keyFor(contractAddress, network), { final: chooseTier(null, fallbackCategory) })
156+
)
157+
return out
158+
}
159+
160+
const lookup = new Map<string, string | null>()
161+
;(data as FeedRiskRow[] | null)?.forEach((row) =>
162+
lookup.set(keyFor(row.proxy_address, row.network), row.risk_status ?? null)
163+
)
164+
165+
feedRequests.forEach(({ contractAddress, network, fallbackCategory }) => {
166+
const key = keyFor(contractAddress, network)
167+
out.set(key, { final: chooseTier(lookup.get(key), fallbackCategory) })
168+
})
169+
170+
return out
171+
} catch {
172+
feedRequests.forEach(({ contractAddress, network, fallbackCategory }) =>
173+
out.set(keyFor(contractAddress, network), { final: chooseTier(null, fallbackCategory) })
174+
)
175+
return out
176+
}
177+
}

src/db/supabase.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createClient } from "@supabase/supabase-js"
2+
3+
const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL
4+
const supabaseKey = import.meta.env.PUBLIC_SUPABASE_ANON_KEY
5+
6+
// Export a function that safely creates the client
7+
export function getSupabaseClient() {
8+
if (!supabaseUrl || !supabaseKey) {
9+
return null
10+
}
11+
return createClient(supabaseUrl, supabaseKey)
12+
}
13+
14+
// Export the client instance (may be null)
15+
export const supabase = getSupabaseClient()

src/features/feeds/components/FeedList.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useGetChainMetadata } from "./useGetChainMetadata.ts"
99
import { ChainMetadata } from "~/features/data/api/index.ts"
1010
import useQueryString from "~/hooks/useQueryString.ts"
1111
import { RefObject } from "preact"
12+
import { getFeedCategories } from "../../../db/feedCategories.js"
1213
import SectionWrapper from "~/components/SectionWrapper/SectionWrapper.tsx"
1314
import button from "@chainlink/design-system/button.module.css"
1415
import { updateTableOfContents } from "~/components/TableOfContents/tocStore.ts"
@@ -137,14 +138,27 @@ export const FeedList = ({
137138
const testnetLastAddr = Number(testnetCurrentPage) * testnetAddrPerPage
138139
const testnetFirstAddr = testnetLastAddr - testnetAddrPerPage
139140

140-
const dataFeedCategory = [
141+
// Dynamic feed categories loaded from Supabase
142+
const [dataFeedCategory, setDataFeedCategory] = useState([
141143
{ key: "low", name: "Low Market Risk" },
142144
{ key: "medium", name: "Medium Market Risk" },
143145
{ key: "high", name: "High Market Risk" },
144146
{ key: "custom", name: "Custom" },
145147
{ key: "new", name: "New Token" },
146148
{ key: "deprecating", name: "Deprecating" },
147-
]
149+
])
150+
151+
// Load dynamic categories from Supabase on component mount
152+
useEffect(() => {
153+
const loadCategories = async () => {
154+
try {
155+
const categories = await getFeedCategories()
156+
setDataFeedCategory(categories)
157+
} catch (error) {}
158+
}
159+
160+
loadCategories()
161+
}, [])
148162
const smartDataTypes = [
149163
{ key: "Proof of Reserve", name: "Proof of Reserve" },
150164
{ key: "NAVLink", name: "NAVLink" },

0 commit comments

Comments
 (0)