Skip to content
Draft
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: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,15 @@ jobs:
path: libs/ts/ui/storybook-static
project_name: blocksense-ui
build_command: yarn workspace @blocksense/ui build-storybook
- project: explorer.blocksense.network
path: apps/explorer.blocksense.network/out
project_name: blocksense-explorer
build_command: yarn workspace @blocksense/explorer.blocksense.network build:with-deps
outputs:
docsDeploymentMessage: ${{ steps.docs-ws-url.outputs.url }}
docsUiDeploymentMessage: ${{ steps.docs-ui-ws-url.outputs.url }}
uiDeploymentMessage: ${{ steps.ui-ws-url.outputs.url }}
explorerDeploymentMessage: ${{ steps.explorer-ws-url.outputs.url }}
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -267,6 +272,14 @@ jobs:
run: |
echo "url=$UI_DEPLOY_URL" >> $GITHUB_OUTPUT

- name: Collect Explorer Deployment URL
id: explorer-ws-url
if: matrix.project == 'explorer.blocksense.network'
env:
EXPLORER_DEPLOY_URL: ${{ steps.deploy-website.outputs.deployment-url }}
run: |
echo "url=$EXPLORER_DEPLOY_URL" >> $GITHUB_OUTPUT

comment_on_pr:
needs: [deploy_websites]
runs-on: self-hosted
Expand All @@ -290,3 +303,4 @@ jobs:
| 🌱 [Documentation](${{ needs.deploy_websites.outputs.docsDeploymentMessage }}) | ${{ steps.datetime.outputs.latest_update }} | ${{ github.sha }} |
| 📖 [Docs UI Components](${{ needs.deploy_websites.outputs.docsUiDeploymentMessage }}) | ${{ steps.datetime.outputs.latest_update }} | ${{ github.sha }} |
| 📝 [UI Components](${{ needs.deploy_websites.outputs.uiDeploymentMessage }}) | ${{ steps.datetime.outputs.latest_update }} | ${{ github.sha }} |
| 🔎 [Explorer](${{ needs.deploy_websites.outputs.explorerDeploymentMessage }}) | ${{ steps.datetime.outputs.latest_update }} | ${{ github.sha }} |
4 changes: 4 additions & 0 deletions .github/workflows/on-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
path: libs/ts/ui/storybook-static
project_name: blocksense-ui
build_command: yarn workspace @blocksense/ui build-storybook
- project: explorer.blocksense.network
path: apps/explorer.blocksense.network/out
project_name: blocksense-explorer
build_command: yarn workspace @blocksense/explorer.blocksense.network build:with-deps
steps:
- uses: actions/checkout@v4

Expand Down
41 changes: 41 additions & 0 deletions apps/explorer.blocksense.network/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
7 changes: 7 additions & 0 deletions apps/explorer.blocksense.network/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
output: 'export',
};

export default nextConfig;
31 changes: 31 additions & 0 deletions apps/explorer.blocksense.network/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@blocksense/explorer.blocksense.network",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@blocksense/base-utils": "workspace:*",
"@blocksense/config-types": "workspace:*",
"@blocksense/contracts": "workspace:*",
"@blocksense/ui": "workspace:*",
"chart.js": "^4.5.0",
"next": "15.4.3",
"react": "19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "19.1.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9.32.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
5 changes: 5 additions & 0 deletions apps/explorer.blocksense.network/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const config = {
plugins: ['@tailwindcss/postcss'],
};

export default config;
17 changes: 17 additions & 0 deletions apps/explorer.blocksense.network/src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use server';
import { CLAggregatorAdapterConsumer } from '@blocksense/contracts/viem';
import type { NetworkName } from '@blocksense/base-utils';

export async function getPriceAndDecimalsAction(
contractAddress: `0x${string}`,
networkParam: NetworkName,
) {
const consumer = CLAggregatorAdapterConsumer.createConsumerByNetworkName(
contractAddress as `0x${string}`,
networkParam,
);
const decimals = await consumer.getDecimals();
const latestAnswer = await consumer.getLatestAnswer();
const price = Number(latestAnswer) / 10 ** decimals;
return { price, decimals };
}
Binary file not shown.
85 changes: 85 additions & 0 deletions apps/explorer.blocksense.network/src/app/feed/[feed]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Metadata } from 'next';

import { readAllEvmDeployments } from '@blocksense/config-types';
import type { NetworkName } from '@blocksense/base-utils';

import Feed from '@/components/Feed';

function getFeedNameAndId(feed: string) {
const feedParts = feed.split('-');
const feedId = feedParts[feedParts.length - 1];
const feedName = feedParts.slice(0, -1).join(' / ').toLocaleUpperCase();
return { feedName, feedId };
}

export async function generateStaticParams() {
const deploymentInfo = await readAllEvmDeployments([
'local',
'somnia-mainnet',
]);

const params: Array<{ feed: string }> = [];
for (const network of Object.keys(deploymentInfo) as Array<NetworkName>) {
const adapter = deploymentInfo[network].contracts.CLAggregatorAdapter;
for (const [feedId, contract] of Object.entries(adapter)) {
const rawName = contract.constructorArgs[0] as string;
const slug = rawName
.toLowerCase()
.replace(/\s+\/\s+/g, '-')
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/-+/g, '-')
.replace(/(^-|-$)/g, '');

params.push({ feed: `${slug}-${feedId}` });
}
}

return params;
}

type MetadataProps = {
params: Promise<{ feed: string }>;
};

export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const { feed } = await params;
const { feedName } = getFeedNameAndId(feed);
return { title: `${feedName} | Blocksense` };
}

type FeedPageProps = {
params: Promise<{ feed: string; network: NetworkName }>;
searchParams: Promise<{ network?: NetworkName }>;
};

export default async function FeedPage({
params,
searchParams,
}: FeedPageProps) {
const { feed } = await params;
const { network } = await searchParams;
const { feedId } = getFeedNameAndId(feed);

const deploymentInfo = await readAllEvmDeployments(['local']);
if (!network || !deploymentInfo[network]) {
return <p>Please select a network.</p>;
}
const feedConfig =
deploymentInfo[network].contracts.CLAggregatorAdapter[feedId];

return (
<section className="flex flex-col gap-6 bg-[var(--gray)] p-8 rounded-3xl w-full h-full">
<h1 className="text-2xl text-center font-bold">
{feedConfig.constructorArgs[0]}
</h1>
<Feed
contractAddress={feedConfig.address as `0x${string}`}
network={network}
feedId={feedId}
/>
</section>
);
}
22 changes: 22 additions & 0 deletions apps/explorer.blocksense.network/src/app/feeds/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Suspense } from 'react';

import {
listEvmNetworks,
readAllEvmDeployments,
} from '@blocksense/config-types';

import { Feeds } from '../../components/Feeds';

export default async function FeedsPage() {
const networks = await listEvmNetworks(['local', 'somnia-mainnet']);
const deploymentInfo = await readAllEvmDeployments([
'local',
'somnia-mainnet',
]);

return (
<Suspense fallback={<p>Loading feeds...</p>}>
<Feeds networks={networks} deploymentInfo={deploymentInfo} />
</Suspense>
);
}
22 changes: 22 additions & 0 deletions apps/explorer.blocksense.network/src/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import 'tailwindcss';

:root {
--black: #0c0c0c;
--white: #f4f3f3;
--gray: #2b2929;
--light-gray: #797979;
}

html,
body {
margin: 0;
width: 100%;
height: 100%;
}

body {
font-family: Arial, Helvetica, sans-serif;
background-color: var(--black);
color: var(--white);
padding: 5rem 7rem;
}
34 changes: 34 additions & 0 deletions apps/explorer.blocksense.network/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';

const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});

const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});

export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
5 changes: 5 additions & 0 deletions apps/explorer.blocksense.network/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirect } from 'next/navigation';

export default function Page() {
redirect('/feeds');
}
68 changes: 68 additions & 0 deletions apps/explorer.blocksense.network/src/components/Feed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import { useEffect, useState } from 'react';

import type { NetworkName } from '@blocksense/base-utils';

import { getPriceAndDecimalsAction } from '@/app/actions';
import { FeedPriceChart } from '@/components/FeedPriceChart';

type FeedProps = {
contractAddress: `0x${string}`;
network: NetworkName;
feedId: string;
};

export default function Feed({ contractAddress, feedId, network }: FeedProps) {
const [price, setPrice] = useState<number | null>(null);
const [decimals, setDecimals] = useState<number | null>(null);
const [lastUpdated, setLastUpdated] = useState<number | null>(null);
const [points, setPoints] = useState<
Array<{ timestamp: number; value: number }>
>([]);

useEffect(() => {
const fetchPrice = async () => {
const { decimals: fetchedDecimals, price: fetchedPrice } =
await getPriceAndDecimalsAction(contractAddress, network);
console.log(`Fetched price: ${fetchedPrice}`);

setPrice(fetchedPrice);
setDecimals(fetchedDecimals);
setLastUpdated(Date.now());
setPoints(state => [
...state,
{ timestamp: Date.now(), value: fetchedPrice },
]);
};

fetchPrice();
const interval = setInterval(fetchPrice, 60000);
return () => clearInterval(interval);
}, [contractAddress, network]);

return (
<>
<section className="flex justify-between items-center">
<article>
<h2 className="text-xl">
<strong>{price || '...'}</strong>
</h2>
<p className="text-[var(--light-gray)] text-sm">
Decimals: {decimals || '...'}
</p>
<p className="text-[var(--light-gray)] text-sm">
Last updated:{' '}
{lastUpdated ? new Date(lastUpdated).toLocaleTimeString() : '...'}
</p>
</article>
<section>
<p className="text-sm text-[var(--white)] font-bold">Id: {feedId}</p>
<p className="text-sm text-[var(--light-gray)]">{contractAddress}</p>
<p className="text-sm text-[var(--light-gray)]">Network: {network}</p>
</section>
</section>
<FeedPriceChart points={points} />
</>
);
}
Loading
Loading