diff --git a/src/app/api/chainhooks/arkadiko/add-liquidity/route.ts b/src/app/api/chainhooks/arkadiko/add-liquidity/route.ts index bc38c83..22429d8 100644 --- a/src/app/api/chainhooks/arkadiko/add-liquidity/route.ts +++ b/src/app/api/chainhooks/arkadiko/add-liquidity/route.ts @@ -1,11 +1,11 @@ import { db } from "@/db/db"; import { type InsertTransaction, transactionTable } from "@/db/schema"; +import { getOrInsertToken } from "@/db/token"; import type { ChainhookPayload, ChainhookReceiptEventFTTransferEvent, ChainhookReceiptEventSTXTransferEvent, } from "@/lib/chainhooks"; -import { getOrInsertToken } from "@/lib/currencies"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/chainhooks/arkadiko/remove-liquidity/route.ts b/src/app/api/chainhooks/arkadiko/remove-liquidity/route.ts index 682e3a8..f1d0e6f 100644 --- a/src/app/api/chainhooks/arkadiko/remove-liquidity/route.ts +++ b/src/app/api/chainhooks/arkadiko/remove-liquidity/route.ts @@ -1,11 +1,11 @@ import { db } from "@/db/db"; import { type InsertTransaction, transactionTable } from "@/db/schema"; +import { getOrInsertToken } from "@/db/token"; import type { ChainhookPayload, ChainhookReceiptEventFTTransferEvent, ChainhookReceiptEventSTXTransferEvent, } from "@/lib/chainhooks"; -import { getOrInsertToken } from "@/lib/currencies"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/chainhooks/stackingdao/deposit/route.ts b/src/app/api/chainhooks/stackingdao/deposit/route.ts index 9ba5530..696ec1c 100644 --- a/src/app/api/chainhooks/stackingdao/deposit/route.ts +++ b/src/app/api/chainhooks/stackingdao/deposit/route.ts @@ -1,12 +1,12 @@ import { db } from "@/db/db"; import { type InsertTransaction, transactionTable } from "@/db/schema"; +import { getOrInsertToken } from "@/db/token"; import { conflictUpdateSetAllColumns } from "@/db/utils"; import type { ChainhookPayload, ChainhookReceiptEventFTMintEvent, ChainhookReceiptEventSTXTransferEvent, } from "@/lib/chainhooks"; -import { getOrInsertToken } from "@/lib/currencies"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/chainhooks/stackingdao/withdraw/route.ts b/src/app/api/chainhooks/stackingdao/withdraw/route.ts index b628f17..11c55d3 100644 --- a/src/app/api/chainhooks/stackingdao/withdraw/route.ts +++ b/src/app/api/chainhooks/stackingdao/withdraw/route.ts @@ -1,12 +1,12 @@ import { db } from "@/db/db"; import { type InsertTransaction, transactionTable } from "@/db/schema"; +import { getOrInsertToken } from "@/db/token"; import { conflictUpdateSetAllColumns } from "@/db/utils"; import type { ChainhookPayload, ChainhookReceiptEventFTBurnEvent, ChainhookReceiptEventSTXTransferEvent, } from "@/lib/chainhooks"; -import { getOrInsertToken } from "@/lib/currencies"; export const dynamic = "force-dynamic"; diff --git a/src/app/api/chainhooks/swap/route.ts b/src/app/api/chainhooks/swap/route.ts index 0645411..3dc6a6f 100644 --- a/src/app/api/chainhooks/swap/route.ts +++ b/src/app/api/chainhooks/swap/route.ts @@ -1,12 +1,12 @@ import { db } from "@/db/db"; import { type InsertTransaction, transactionTable } from "@/db/schema"; +import { getOrInsertToken } from "@/db/token"; import { conflictUpdateSetAllColumns } from "@/db/utils"; import type { ChainhookPayload, ChainhookReceiptEventFTTransferEvent, ChainhookReceiptEventSTXTransferEvent, } from "@/lib/chainhooks"; -import { getOrInsertToken } from "@/lib/currencies"; import type { Protocol } from "@/lib/protocols"; export const dynamic = "force-dynamic"; diff --git a/src/app/protocols/[protocol]/page.tsx b/src/app/protocols/[protocol]/page.tsx index a42df50..d67bbbc 100644 --- a/src/app/protocols/[protocol]/page.tsx +++ b/src/app/protocols/[protocol]/page.tsx @@ -1,4 +1,6 @@ +import { ProtocolInfo } from "@/components/Protocol/ProtocolInfo"; import { UniqueUsersBarChart } from "@/components/Stats/UniqueUsersBarChart"; +import { DepositWithdrawBarChart } from "@/components/Stats/stackingdao/DepositsWithdrawBarChart"; import { TransactionRow } from "@/components/Transaction/TransactionRow"; import { getTransactions, getTransactionsStats } from "@/db/transactions"; import { @@ -13,13 +15,10 @@ import { Card, Container, Heading, - IconButton, Separator, Text, } from "@radix-ui/themes"; -import { IconBrandX, IconWorld } from "@tabler/icons-react"; import type { Metadata } from "next"; -import Image from "next/image"; import NextLink from "next/link"; import { notFound } from "next/navigation"; import { Fragment, Suspense } from "react"; @@ -61,41 +60,11 @@ export default async function ProtocolPage({ getTransactions({ protocol, action: searchParams.action }), getTransactionsStats({ protocol }), ]); - const protocolInfo = protocolsInfo[protocol]; const protocolActions = protocolsActions[protocol]; return ( -
- {`${protocol} -
- - {protocolInfo.name} - - - {protocolInfo.description} - -
- - - - - - - - - - -
-
-
+
@@ -120,6 +89,12 @@ export default async function ProtocolPage({ + {protocol === "stackingdao" ? ( + + + + ) : null} +
Transactions diff --git a/src/components/Protocol/ProtocolInfo.tsx b/src/components/Protocol/ProtocolInfo.tsx new file mode 100644 index 0000000..457a35a --- /dev/null +++ b/src/components/Protocol/ProtocolInfo.tsx @@ -0,0 +1,45 @@ +import { Protocol, protocolsInfo } from "@/lib/protocols"; +import { Heading, IconButton, Text } from "@radix-ui/themes"; +import { IconBrandX, IconWorld } from "@tabler/icons-react"; +import Image from "next/image"; + +interface ProtocolInfoProps { + protocol: Protocol; +} + +export const ProtocolInfo = ({ protocol }: ProtocolInfoProps) => { + const protocolInfo = protocolsInfo[protocol]; + + return ( +
+ {`${protocol} +
+ + {protocolInfo.name} + + + {protocolInfo.description} + +
+ + + + + + + + + + +
+
+
+ ); +}; diff --git a/src/components/Stats/UniqueUsersBarChart/UniqueUsersBarChartClient.tsx b/src/components/Stats/UniqueUsersBarChart/UniqueUsersBarChartClient.tsx index d36086a..32dc963 100644 --- a/src/components/Stats/UniqueUsersBarChart/UniqueUsersBarChartClient.tsx +++ b/src/components/Stats/UniqueUsersBarChart/UniqueUsersBarChartClient.tsx @@ -1,5 +1,6 @@ "use client"; import { BarChart } from "@/components/ui/BarChart"; +import { numberValueFormatter } from "@/components/ui/utils"; import type { Protocol } from "@/lib/protocols"; import { Card, Inset, Separator, Text } from "@radix-ui/themes"; @@ -39,6 +40,7 @@ export const UniqueUsersBarChartClient = ({ index="date" categories={[protocol]} colors={["orange"]} + valueFormatter={numberValueFormatter} /> ); diff --git a/src/components/Stats/stackingdao/DepositsWithdrawBarChart/DepositWithdrawBarChartClient.tsx b/src/components/Stats/stackingdao/DepositsWithdrawBarChart/DepositWithdrawBarChartClient.tsx new file mode 100644 index 0000000..830a794 --- /dev/null +++ b/src/components/Stats/stackingdao/DepositsWithdrawBarChart/DepositWithdrawBarChartClient.tsx @@ -0,0 +1,38 @@ +"use client"; +import { BarChart } from "@/components/ui/BarChart"; +import { displayPrice } from "@/lib/currencies"; +import { Card, Inset, Separator, Text } from "@radix-ui/themes"; + +interface DepositWithdrawBarChartClientProps { + data: { + date: string; + withdrawals: number; + deposits: number; + }[]; +} + +export const DepositWithdrawBarChartClient = ({ + data, +}: DepositWithdrawBarChartClientProps) => { + return ( + + + Deposits and Withdrawals in STX + + + Amount of deposits and withdrawals made by users per month + + + + + displayPrice(value, 6)} + /> + + ); +}; diff --git a/src/components/Stats/stackingdao/DepositsWithdrawBarChart/index.tsx b/src/components/Stats/stackingdao/DepositsWithdrawBarChart/index.tsx new file mode 100644 index 0000000..5bdf369 --- /dev/null +++ b/src/components/Stats/stackingdao/DepositsWithdrawBarChart/index.tsx @@ -0,0 +1,35 @@ +import { db } from "@/db/db"; +import { transactionTable } from "@/db/schema"; +import { eq, sql } from "drizzle-orm"; +import { DepositWithdrawBarChartClient } from "./DepositWithdrawBarChartClient"; + +const getData = async () => { + const query = db + .select({ + month: sql`strftime('%Y-%m', timestamp, 'unixepoch') as month`, + depositsAmount: sql`sum(case when action = 'stackingdao-deposit' then json->>'outAmount' else 0 end) as depositsAmount`, + withdrawalsAmount: sql`sum(case when action = 'stackingdao-withdraw' then json->>'inAmount' else 0 end) as withdrawalsAmount`, + }) + .from(transactionTable) + .where(eq(transactionTable.protocol, "stackingdao")) + .groupBy(sql`month`); + + const stats = await query; + return stats; +}; + +export const DepositWithdrawBarChart = async () => { + const stats = await getData(); + + const formattedData: { + date: string; + withdrawals: number; + deposits: number; + }[] = stats.map((d) => ({ + date: d.month, + withdrawals: d.withdrawalsAmount, + deposits: d.depositsAmount, + })); + + return ; +}; diff --git a/src/components/ui/BarChart.tsx b/src/components/ui/BarChart.tsx index 64aa5ca..f7416e0 100644 --- a/src/components/ui/BarChart.tsx +++ b/src/components/ui/BarChart.tsx @@ -9,7 +9,7 @@ import { XAxis, YAxis, } from "recharts"; -import { constructCategoryColors } from "./utils"; +import { constructCategoryColors, defaultValueFormatter } from "./utils"; interface BarChartProps { className?: string; @@ -18,6 +18,7 @@ interface BarChartProps { categories: string[]; stack?: boolean; colors: string[]; + valueFormatter?: (value: number | string) => string; } export const BarChart = ({ @@ -27,6 +28,7 @@ export const BarChart = ({ categories, stack, colors, + valueFormatter = defaultValueFormatter, }: BarChartProps) => { const categoryColors = constructCategoryColors(categories, colors); @@ -64,8 +66,9 @@ export const BarChart = ({ stroke="" className="fill-gray-11 text-1" tickMargin={8} + tickFormatter={valueFormatter} /> - cursor={{ fill: "var(--gray-11)", opacity: "0.10" }} content={({ label, payload }) => (
@@ -87,7 +90,7 @@ export const BarChart = ({ {p.name}:{" "} - {p.value?.toLocaleString("en-US")} + {p.value ? valueFormatter(p.value) : ""}
diff --git a/src/components/ui/utils.ts b/src/components/ui/utils.ts index cd10eac..3b5df9a 100644 --- a/src/components/ui/utils.ts +++ b/src/components/ui/utils.ts @@ -10,6 +10,10 @@ export const themeColors: { fillColor: "fill-orange-9", bgColor: "bg-orange-9", }, + indigo: { + fillColor: "fill-indigo-9", + bgColor: "bg-indigo-9", + }, }; export const constructCategoryColors = ( @@ -22,3 +26,11 @@ export const constructCategoryColors = ( }); return categoryColors; }; + +export const defaultValueFormatter = (value: number | string) => { + return value.toString(); +}; + +export const numberValueFormatter = (value: number | string) => { + return value.toLocaleString("en-US"); +}; diff --git a/src/db/token.ts b/src/db/token.ts new file mode 100644 index 0000000..bb2b849 --- /dev/null +++ b/src/db/token.ts @@ -0,0 +1,55 @@ +import { callReadOnlyFunction, cvToValue } from "@stacks/transactions"; +import { eq } from "drizzle-orm"; +import { db } from "./db"; +import { tokenTable } from "./schema"; + +/** + * Get a token from the database or insert it if it doesn't exist. + */ +export const getOrInsertToken = async (tokenId: string) => { + const tokens = await db + .select() + .from(tokenTable) + .where(eq(tokenTable.id, tokenId)); + let token = tokens[0]; + if (!token) { + // Special case for STX + if (tokenId === "STX") { + token = { + id: "STX", + symbol: "STX", + decimals: 6, + }; + await db.insert(tokenTable).values(token); + return; + } + + const smartContract = tokenId.split("::")[0]; + const [contractAddress, contractName] = smartContract.split("."); + const resultDecimals = await callReadOnlyFunction({ + contractAddress, + contractName, + functionName: "get-decimals", + functionArgs: [], + network: "mainnet", + senderAddress: "SP2EVYKET55QH40RAZE5PVZ363QX0X6BSRP4C7H0W", + }); + + const resultSymbol = await callReadOnlyFunction({ + contractAddress, + contractName, + functionName: "get-symbol", + functionArgs: [], + network: "mainnet", + senderAddress: "SP2EVYKET55QH40RAZE5PVZ363QX0X6BSRP4C7H0W", + }); + + token = { + id: tokenId, + symbol: cvToValue(resultSymbol).value, + decimals: Number(cvToValue(resultDecimals).value), + }; + await db.insert(tokenTable).values(token); + } + return token; +}; diff --git a/src/lib/currencies.ts b/src/lib/currencies.ts index ac500c8..f54685c 100644 --- a/src/lib/currencies.ts +++ b/src/lib/currencies.ts @@ -1,10 +1,5 @@ -import { tokenTable } from "@/db/schema"; -import { callReadOnlyFunction, cvToValue } from "@stacks/transactions"; -import { eq } from "drizzle-orm"; -import { db } from "../db/db"; - export const displayPrice = ( - price: bigint | string, + price: bigint | number | string, decimals: number, ): string => { const priceNumber = Number(price) / 10 ** decimals; @@ -17,54 +12,3 @@ export const displayPrice = ( notation: "compact", }); }; - -/** - * Get a token from the database or insert it if it doesn't exist. - */ -export const getOrInsertToken = async (tokenId: string) => { - const tokens = await db - .select() - .from(tokenTable) - .where(eq(tokenTable.id, tokenId)); - let token = tokens[0]; - if (!token) { - // Special case for STX - if (tokenId === "STX") { - token = { - id: "STX", - symbol: "STX", - decimals: 6, - }; - await db.insert(tokenTable).values(token); - return; - } - - const smartContract = tokenId.split("::")[0]; - const [contractAddress, contractName] = smartContract.split("."); - const resultDecimals = await callReadOnlyFunction({ - contractAddress, - contractName, - functionName: "get-decimals", - functionArgs: [], - network: "mainnet", - senderAddress: "SP2EVYKET55QH40RAZE5PVZ363QX0X6BSRP4C7H0W", - }); - - const resultSymbol = await callReadOnlyFunction({ - contractAddress, - contractName, - functionName: "get-symbol", - functionArgs: [], - network: "mainnet", - senderAddress: "SP2EVYKET55QH40RAZE5PVZ363QX0X6BSRP4C7H0W", - }); - - token = { - id: tokenId, - symbol: cvToValue(resultSymbol).value, - decimals: Number(cvToValue(resultDecimals).value), - }; - await db.insert(tokenTable).values(token); - } - return token; -};