diff --git a/package-lock.json b/package-lock.json
index e2fc7f97..67fee4c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -55,7 +55,7 @@
},
"devDependencies": {
"@keplr-wallet/types": "^0.12.43",
- "@types/mixpanel-browser": "^2.38.1",
+ "@types/mixpanel-browser": "^2.50.2",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.3.1",
"husky": "^9.1.6",
@@ -9441,9 +9441,9 @@
}
},
"node_modules/@types/mixpanel-browser": {
- "version": "2.47.4",
- "resolved": "https://registry.npmjs.org/@types/mixpanel-browser/-/mixpanel-browser-2.47.4.tgz",
- "integrity": "sha512-wAwhSaIk//XY+O5Y8vnlb0Dc9Rpt0s+yHRXf1cGIYM4G5efzRjNeqWEqROf4qzP9FUgVE2M0Z8Pg9BV8Dt46yw==",
+ "version": "2.50.2",
+ "resolved": "https://registry.npmjs.org/@types/mixpanel-browser/-/mixpanel-browser-2.50.2.tgz",
+ "integrity": "sha512-Iw8cBzplUPfHoeYuasqeYwdbGTNXhN+5kFT9kU+C7zm0NtaiPpKoiuzITr2ZH9KgBsWi2MbG0FOzIg9sQepauQ==",
"dev": true
},
"node_modules/@types/ms": {
diff --git a/package.json b/package.json
index 76e3beb9..e2f9df4a 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
},
"devDependencies": {
"@keplr-wallet/types": "^0.12.43",
- "@types/mixpanel-browser": "^2.38.1",
+ "@types/mixpanel-browser": "^2.50.2",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.3.1",
"husky": "^9.1.6",
diff --git a/src/context/APIContext.tsx b/src/context/APIContext.tsx
index 2d256a27..5d0a6282 100644
--- a/src/context/APIContext.tsx
+++ b/src/context/APIContext.tsx
@@ -4,7 +4,7 @@ import { SecretNetworkClient } from 'secretjs'
import { useUserPreferencesStore } from 'store/UserPreferences'
import { Currency } from 'types/Currency'
import { Nullable } from 'types/Nullable'
-import { allTokens, coinGeckoCurrencyMap, sortDAppsArray } from 'utils/commons'
+import { allTokens, bech32PrefixToChainName, coinGeckoCurrencyMap, sortDAppsArray } from 'utils/commons'
import { SECRET_LCD, SECRET_CHAIN_ID } from 'utils/config'
const APIContext = createContext(null)
@@ -125,6 +125,7 @@ const APIContextProvider = ({ children }: any) => {
const [analyticsData2, setAnalyticsData2] = useState()
const [analyticsData3, setAnalyticsData3] = useState()
const [analyticsData4, setAnalyticsData4] = useState()
+ const [analyticsData5, setAnalyticsData5] = useState()
const [L5AnalyticsApiData, setL5AnalyticsApiData] = useState()
const [volume, setVolume] = useState(Number)
const [marketCap, setMarketCap] = useState(Number)
@@ -231,7 +232,25 @@ const APIContextProvider = ({ children }: any) => {
.then((response) => (response as any).json())
.catch((error: any) => console.error(error))
.then((response) => {
- setAnalyticsData4(response)
+ const filteredData = response
+ .filter((entry: any) => entry.IBC_Counterpart !== null && entry.IBC_Counterpart !== 'secret')
+ .map((entry: any) => {
+ const chainName = bech32PrefixToChainName.get(entry.IBC_Counterpart) || entry.IBC_Counterpart
+ return {
+ ...entry,
+ IBC_Counterpart: chainName
+ }
+ })
+ setAnalyticsData4(filteredData)
+ })
+
+ const API_DATA_SECRET_5 = `https://dashboardstats.secretsaturn.net/source/weekly_contract_usage_stats/data.json`
+ fetch(API_DATA_SECRET_5)
+ .catch((error: any) => console.error(error))
+ .then((response) => (response as any).json())
+ .catch((error: any) => console.error(error))
+ .then((response) => {
+ setAnalyticsData5(response)
})
const LAVENDERFIVE_API_URL_SECRET_STATUS = `https://api.lavenderfive.com/networks/secretnetwork`
@@ -396,6 +415,7 @@ const APIContextProvider = ({ children }: any) => {
analyticsData2,
analyticsData3,
analyticsData4,
+ analyticsData5,
L5AnalyticsApiData,
bondedToken,
notBondedToken,
diff --git a/src/pages/analytics/Analytics.tsx b/src/pages/analytics/Analytics.tsx
index 9c5872e1..2ee1fb82 100644
--- a/src/pages/analytics/Analytics.tsx
+++ b/src/pages/analytics/Analytics.tsx
@@ -11,9 +11,11 @@ import RelayerChartWithDateSlider from './components/RelayerChartWithDateSlider'
import RelayerChartWithChainSlider from './components/RelayerChartWithChainSlider'
import RelayerChartWithProviderSlider from './components/RelayerChartWithProviderSlider'
import RelayerChartTotal from './components/RelayerChartTotal'
+import WeeklyContractsChart from './components/WeeklyContractsChart'
function Analytics() {
- const { L5AnalyticsApiData, analyticsData1, analyticsData2, analyticsData3, analyticsData4 } = useContext(APIContext)
+ const { L5AnalyticsApiData, analyticsData1, analyticsData2, analyticsData3, analyticsData4, analyticsData5 } =
+ useContext(APIContext)
useEffect(() => {
trackMixPanelEvent('Open Analytics Tab')
@@ -65,6 +67,13 @@ function Analytics() {
>
) : null}
+ {analyticsData5 ? (
+ <>
+
+
+
+ >
+ ) : null}
{analyticsData4 ? (
<>
diff --git a/src/pages/analytics/components/RelayerChartTotal.tsx b/src/pages/analytics/components/RelayerChartTotal.tsx
index 5d0e74fc..dee4af81 100644
--- a/src/pages/analytics/components/RelayerChartTotal.tsx
+++ b/src/pages/analytics/components/RelayerChartTotal.tsx
@@ -1,5 +1,5 @@
import { useContext, useMemo } from 'react'
-import { bech32PrefixToChainName, formatNumber } from 'utils/commons'
+import { formatNumber } from 'utils/commons'
import Tooltip from '@mui/material/Tooltip'
import {
Chart as ChartJS,
@@ -18,13 +18,6 @@ import { APIContext } from 'context/APIContext'
ChartJS.register(CategoryScale, LinearScale, BarElement, ChartTooltip, Legend, BarController)
-type Entry = {
- Date: string
- IBC_Counterpart: string
- Relayer: string
- Transactions: number
-}
-
export default function RelayerChartTotal() {
const { theme } = useUserPreferencesStore()
const { analyticsData4 } = useContext(APIContext)
@@ -35,12 +28,8 @@ export default function RelayerChartTotal() {
const datesSet = new Set
()
const dataMatrix: Record = {}
- const filteredData = analyticsData4.filter(
- (entry: Entry) => entry.IBC_Counterpart !== null && entry.IBC_Counterpart !== 'secret'
- )
-
// Collect and sort dates
- for (const entry of filteredData) {
+ for (const entry of analyticsData4) {
const date = new Date(entry.Date).toISOString().split('T')[0]
datesSet.add(date)
}
@@ -56,13 +45,11 @@ export default function RelayerChartTotal() {
)
// Process entries and populate dataMatrix
- for (const entry of filteredData) {
+ for (const entry of analyticsData4) {
const date = new Date(entry.Date).toISOString().split('T')[0]
const dateIndex = dateIndexMap[date]
- const label = `${bech32PrefixToChainName.get(entry.IBC_Counterpart) || entry.IBC_Counterpart} - ${
- entry.Relayer || 'Other'
- }`
+ const label = `${entry.IBC_Counterpart} - ${entry.Relayer || 'Other'}`
// Initialize dataMatrix[label] if it doesn't exist
if (!dataMatrix[label]) {
diff --git a/src/pages/analytics/components/RelayerChartWithChainSlider.tsx b/src/pages/analytics/components/RelayerChartWithChainSlider.tsx
index 08b3a1dd..ef01397d 100644
--- a/src/pages/analytics/components/RelayerChartWithChainSlider.tsx
+++ b/src/pages/analytics/components/RelayerChartWithChainSlider.tsx
@@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from 'react'
-import { bech32PrefixToChainName, formatNumber } from 'utils/commons'
+import { formatNumber } from 'utils/commons'
import Tooltip from '@mui/material/Tooltip'
import Slider from '@mui/material/Slider'
import {
@@ -57,10 +57,10 @@ export default function RelayerChartWithChainSlider() {
}
// Sort dates
- const sortedDates = Array.from(datesSet).sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
+ const sortedDates = Array.from(datesSet).sort()
// Sort relayers
- const sortedRelayers = Array.from(relayersSet).sort((a, b) => a.localeCompare(b))
+ const sortedRelayers = Array.from(relayersSet).sort()
// Create datasets for each relayer
const datasets = sortedRelayers.map((relayer) => {
@@ -84,16 +84,12 @@ export default function RelayerChartWithChainSlider() {
// Process data grouped by chain
const chainMap: Record = {}
- analyticsData4
- .filter((entry: Entry) => entry.IBC_Counterpart !== null && entry.IBC_Counterpart !== 'secret')
- .forEach((entry: Entry) => {
- const chainBech32Prefix = entry.IBC_Counterpart
- const chainName = bech32PrefixToChainName.get(chainBech32Prefix) || chainBech32Prefix
- chainMap[chainName] ||= []
- chainMap[chainName].push(entry)
- })
+ analyticsData4.forEach((entry: Entry) => {
+ chainMap[entry.IBC_Counterpart] ||= []
+ chainMap[entry.IBC_Counterpart].push(entry)
+ })
- const sortedChains = Object.keys(chainMap).sort((a, b) => a.localeCompare(b))
+ const sortedChains = Object.keys(chainMap).sort()
setChainLabels(sortedChains)
setChainMap(chainMap)
diff --git a/src/pages/analytics/components/RelayerChartWithDateSlider.tsx b/src/pages/analytics/components/RelayerChartWithDateSlider.tsx
index c2820bea..2a5330d8 100644
--- a/src/pages/analytics/components/RelayerChartWithDateSlider.tsx
+++ b/src/pages/analytics/components/RelayerChartWithDateSlider.tsx
@@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from 'react'
-import { bech32PrefixToChainName, formatNumber } from 'utils/commons'
+import { formatNumber } from 'utils/commons'
import Tooltip from '@mui/material/Tooltip'
import Slider from '@mui/material/Slider'
import {
@@ -37,13 +37,11 @@ export default function RelayerChartWithDateSlider() {
// Process data grouped by date
const dateMap: Record = {}
- analyticsData4
- .filter((entry: Entry) => entry.IBC_Counterpart !== null && entry.IBC_Counterpart !== 'secret')
- .forEach((entry: Entry) => {
- const date = new Date(entry.Date).toISOString().split('T')[0]
- dateMap[date] ||= []
- dateMap[date].push(entry)
- })
+ analyticsData4.forEach((entry: Entry) => {
+ const date = new Date(entry.Date).toISOString().split('T')[0]
+ dateMap[date] ||= []
+ dateMap[date].push(entry)
+ })
const sortedDates = Object.keys(dateMap).sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
setDates(sortedDates)
@@ -77,23 +75,13 @@ export default function RelayerChartWithDateSlider() {
chainMap.set(chainBech32Prefix, (chainMap.get(chainBech32Prefix) || 0) + entry.Transactions)
}
- // Prepare an array of { prefix, label } pairs
- const prefixLabelPairs = Array.from(chainsSet).map((prefix) => ({
- prefix,
- label: bech32PrefixToChainName.get(prefix) || prefix
- }))
-
- // Sort prefixLabelPairs by label alphabetically
- prefixLabelPairs.sort((a, b) => a.label.localeCompare(b.label))
-
- // Extract the sorted bech32Prefixes and labels
- const sortedBech32Prefixes = prefixLabelPairs.map((pair) => pair.prefix)
- const labels = prefixLabelPairs.map((pair) => pair.label)
+ // Sort the prefixes alphabetically and use them as labels
+ const labels = Array.from(chainsSet).sort()
// Create datasets for each relayer
const datasets = Array.from(relayersSet).map((relayer) => {
const chainMap = dataMatrix.get(relayer)!
- const data = sortedBech32Prefixes.map((prefix) => chainMap.get(prefix) || 0)
+ const data = labels.map((prefix) => chainMap.get(prefix) || 0)
return {
label: relayer,
data,
diff --git a/src/pages/analytics/components/RelayerChartWithProviderSlider.tsx b/src/pages/analytics/components/RelayerChartWithProviderSlider.tsx
index c80eb675..cd6b6972 100644
--- a/src/pages/analytics/components/RelayerChartWithProviderSlider.tsx
+++ b/src/pages/analytics/components/RelayerChartWithProviderSlider.tsx
@@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from 'react'
-import { bech32PrefixToChainName, formatNumber } from 'utils/commons'
+import { formatNumber } from 'utils/commons'
import Tooltip from '@mui/material/Tooltip'
import Slider from '@mui/material/Slider'
import {
@@ -38,15 +38,13 @@ export default function RelayerChartWithProviderSlider() {
// Process data grouped by relayer
const relayerMap: Record = {}
- analyticsData4
- .filter((entry: Entry) => entry.IBC_Counterpart !== null && entry.IBC_Counterpart !== 'secret')
- .forEach((entry: Entry) => {
- const relayer = entry.Relayer || 'Other'
- relayerMap[relayer] ||= []
- relayerMap[relayer].push(entry)
- })
+ analyticsData4.forEach((entry: Entry) => {
+ const relayer = entry.Relayer || 'Other'
+ relayerMap[relayer] ||= []
+ relayerMap[relayer].push(entry)
+ })
- const sortedRelayers = Object.keys(relayerMap).sort((a, b) => a.localeCompare(b))
+ const sortedRelayers = Object.keys(relayerMap).sort()
setRelayers(sortedRelayers)
// Calculate total transactions per relayer
@@ -59,7 +57,7 @@ export default function RelayerChartWithProviderSlider() {
const maxTotal = Math.max(...Object.values(relayerTotals))
const topRelayers = Object.keys(relayerTotals).filter((relayer) => relayerTotals[relayer] === maxTotal)
// If multiple relayers have the same max total, choose the first one alphabetically
- const defaultRelayer = topRelayers.sort((a, b) => a.localeCompare(b))[0]
+ const defaultRelayer = topRelayers.sort()[0]
// Find the index of the default relayer
const defaultIndex = sortedRelayers.indexOf(defaultRelayer)
@@ -104,30 +102,18 @@ export default function RelayerChartWithProviderSlider() {
dateMap.set(date, (dateMap.get(date) || 0) + entry.Transactions)
}
- // Sort dates
+ // Sort dates and chains
const sortedDates = Array.from(datesSet).sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
-
- // Prepare an array of { prefix, label } pairs for chains
- const prefixLabelPairs = Array.from(chainsSet).map((prefix) => ({
- prefix,
- label: bech32PrefixToChainName.get(prefix) || prefix
- }))
-
- // Sort prefixLabelPairs by label alphabetically
- prefixLabelPairs.sort((a, b) => a.label.localeCompare(b.label))
-
- // Extract the sorted bech32Prefixes and labels
- const sortedBech32Prefixes = prefixLabelPairs.map((pair) => pair.prefix)
- const chainLabels = prefixLabelPairs.map((pair) => pair.label)
+ const sortedChains = Array.from(chainsSet).sort((a, b) => a.localeCompare(b))
// Create datasets for each chain
- const datasets = sortedBech32Prefixes.map((chainPrefix, index) => {
+ const datasets = sortedChains.map((chainPrefix) => {
const dateMap = dataMatrix.get(chainPrefix)!
const data = sortedDates.map((date) => dateMap.get(date) || 0)
return {
- label: chainLabels[index],
+ label: chainPrefix,
data,
- backgroundColor: getColorFromChain(chainLabels[index])
+ backgroundColor: getColorFromChain(chainPrefix)
}
})
diff --git a/src/pages/analytics/components/WeeklyContractsChart.tsx b/src/pages/analytics/components/WeeklyContractsChart.tsx
new file mode 100644
index 00000000..7ed62454
--- /dev/null
+++ b/src/pages/analytics/components/WeeklyContractsChart.tsx
@@ -0,0 +1,199 @@
+import { useContext, useEffect, useState } from 'react'
+import { formatNumber } from 'utils/commons'
+import { APIContext } from 'context/APIContext'
+import Tooltip from '@mui/material/Tooltip'
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Tooltip as ChartTooltip,
+ BarController,
+ Legend,
+ Title,
+ ChartOptions,
+ ChartData,
+ ChartDataset
+} from 'chart.js'
+import { Bar } from 'react-chartjs-2'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
+import { useUserPreferencesStore } from 'store/UserPreferences'
+
+ChartJS.register(CategoryScale, LinearScale, BarElement, BarController, ChartTooltip, Legend, Title)
+
+type Data = {
+ Date: string
+ contract_admin: string | null
+ contract_creator: string | null
+ contract_name: string
+ num_transactions: number
+ num_wallets: number
+}
+
+export default function WeeklyContractsChart() {
+ const { analyticsData5 } = useContext(APIContext)
+ const { theme } = useUserPreferencesStore()
+ const [chartData, setChartData] = useState | null>(null)
+
+ useEffect(() => {
+ if (analyticsData5) {
+ // Initialize data structures
+ const datesSet = new Set()
+ const contractNamesSet = new Set()
+ const dataByDateAndContract: Record> = {}
+ const totalTransactionsPerContract: Record = {}
+
+ // Process analyticsData5 in a single pass
+ analyticsData5.forEach((item: Data) => {
+ datesSet.add(item.Date)
+ contractNamesSet.add(item.contract_name)
+
+ // Accumulate num_transactions per date and contract
+ if (!dataByDateAndContract[item.Date]) {
+ dataByDateAndContract[item.Date] = {}
+ }
+ if (!dataByDateAndContract[item.Date][item.contract_name]) {
+ dataByDateAndContract[item.Date][item.contract_name] = 0
+ }
+ dataByDateAndContract[item.Date][item.contract_name] += item.num_transactions
+
+ // Accumulate total num_transactions per contract
+ if (!totalTransactionsPerContract[item.contract_name]) {
+ totalTransactionsPerContract[item.contract_name] = 0
+ }
+ totalTransactionsPerContract[item.contract_name] += item.num_transactions
+ })
+
+ // Convert sets to arrays and sort dates
+ const dates = Array.from(datesSet).sort((a, b) => new Date(a).getTime() - new Date(b).getTime())
+ const contractNames = Array.from(contractNamesSet)
+
+ // Sort contracts by total transactions and pick top N
+ const topN = 50
+ const sortedContractNames = contractNames.sort(
+ (a, b) => totalTransactionsPerContract[b] - totalTransactionsPerContract[a]
+ )
+ const topContractNames = sortedContractNames.slice(0, topN)
+
+ // Create datasets for top contracts
+ const datasets: ChartDataset<'bar', number[]>[] = topContractNames.map((contractName, index) => {
+ const data = dates.map((date) => dataByDateAndContract[date]?.[contractName] || 0)
+ return {
+ label: contractName,
+ data,
+ backgroundColor: getColorFromString(contractName),
+ stack: 'Stack 0'
+ }
+ })
+
+ // Compute 'Others' data
+ const othersData = dates.map((date) => {
+ const contractsData = dataByDateAndContract[date] || {}
+ return Object.entries(contractsData).reduce((total, [name, value]) => {
+ return topContractNames.includes(name) ? total : total + value
+ }, 0)
+ })
+
+ // Add 'Others' dataset if applicable
+ if (othersData.some((value) => value > 0)) {
+ datasets.push({
+ label: 'Others',
+ data: othersData,
+ backgroundColor: '#cccccc',
+ stack: 'Stack 0'
+ })
+ }
+
+ // Prepare chart data
+ const preparedChartData: ChartData<'bar', number[], string> = {
+ labels: dates.map((date) =>
+ new Date(date).toLocaleDateString(undefined, {
+ year: '2-digit',
+ month: '2-digit',
+ day: '2-digit'
+ })
+ ),
+ datasets
+ }
+
+ setChartData(preparedChartData)
+ }
+ }, [analyticsData5, theme])
+
+ function getColorFromString(str: string) {
+ // Generate a unique color based on the combo string
+ let hash = 0
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash)
+ }
+ const color = `#${('000000' + (hash & 0xffffff).toString(16)).slice(-6)}`
+ return color
+ }
+
+ const options = {
+ responsive: true,
+ animation: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ stacked: true,
+ ticks: {
+ color: theme === 'dark' ? '#fff' : '#000',
+ autoSkip: false,
+ maxRotation: 90,
+ minRotation: 45
+ },
+ grid: {
+ display: false
+ }
+ },
+ y: {
+ beginAtZero: true,
+ stacked: true,
+ ticks: {
+ color: theme === 'dark' ? '#fff' : '#000',
+ callback: (value: number) => formatNumber(value, 2)
+ },
+ grid: {
+ color: theme === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)',
+ display: true
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ xAlign: 'center',
+ yAlign: 'bottom',
+ backgroundColor: theme === 'dark' ? '#333' : '#fff',
+ titleColor: theme === 'dark' ? '#fff' : '#000',
+ bodyColor: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ }
+
+ return (
+ <>
+
+
+ Weekly Transactions per Contract
+
+
+
+
+
+
+
+
+
+
+ {chartData && chartData.datasets && chartData.datasets.length > 0 ? (
+
+ ) : null}
+
+ >
+ )
+}
diff --git a/src/utils/commons.ts b/src/utils/commons.ts
index e345e00e..80d4a903 100644
--- a/src/utils/commons.ts
+++ b/src/utils/commons.ts
@@ -22,6 +22,7 @@ export const allTokens = tokens.concat(snips).concat(ICSTokens)
// Cache the mapping from bech32 prefixes to chain names
export const bech32PrefixToChainName: Map = new Map()
+
for (const chainInfo of Object.values(chains)) {
bech32PrefixToChainName.set(chainInfo.bech32_prefix, chainInfo.chain_name)
}