diff --git a/.changeset/eleven-numbers-kick.md b/.changeset/eleven-numbers-kick.md new file mode 100644 index 00000000..4fda4166 --- /dev/null +++ b/.changeset/eleven-numbers-kick.md @@ -0,0 +1,5 @@ +--- +"stackspulse": patch +--- + +Add "Data" section in about page. diff --git a/.changeset/nice-lizards-happen.md b/.changeset/nice-lizards-happen.md new file mode 100644 index 00000000..450a0da4 --- /dev/null +++ b/.changeset/nice-lizards-happen.md @@ -0,0 +1,5 @@ +--- +"@stackspulse/server": minor +--- + +Create new `/api/tokens/resolve` route. diff --git a/.changeset/proud-laws-tap.md b/.changeset/proud-laws-tap.md new file mode 100644 index 00000000..e37926e3 --- /dev/null +++ b/.changeset/proud-laws-tap.md @@ -0,0 +1,5 @@ +--- +"@stackspulse/server": minor +--- + +Create new `/api/tokens/markets` route. diff --git a/.changeset/tricky-mirrors-occur.md b/.changeset/tricky-mirrors-occur.md new file mode 100644 index 00000000..8267a4ba --- /dev/null +++ b/.changeset/tricky-mirrors-occur.md @@ -0,0 +1,5 @@ +--- +"stackspulse": minor +--- + +Create new token list page. diff --git a/apps/server/.env.production.local b/apps/server/.env.production.local index 08aafd82..ce126ce7 100644 --- a/apps/server/.env.production.local +++ b/apps/server/.env.production.local @@ -10,3 +10,4 @@ ADMIN_API_TOKEN="encrypted:BIbtsUXc1YZP1rJDw13H4m/BWWUxRdJkuJFV5EZUMYELYQHhmF91g TURSO_DATABASE_URL="encrypted:BFROeaR/Lo5esJOuxqWc7/NdVpxdz8EykAovi9W08PANkfxIsxDLgiOLPB9FUend3EIYA9M7XNOkxxb4jyZrqMwq0qnkQyEOSSIIDQOH+KTnSR6l+RSKItOFYvRSt2MhSSUzCLzf9z9Es55Ut3CmDZuZN2u3hutKNavTo3P62B4gzTGmkmuSaUaIxAOa5Q==" TURSO_AUTH_TOKEN="encrypted:BI9tCPv11ZM3cBZHGXMctKpHDo+cpW6Sq7c3YYoXxNag8PpVSiGGQQMWLtYZqy4OrFauYJtSp+ehyoYuW+ZburNKZs18aaZnOaBzkwZfQG00jvOhfS7k6XhMDOj46Is/zSRS88pbI+YBLxhLQH/Sz7bwn7PGIp72H06C4m/qEyPMpKoRUS9hP1YV86FPZqasGPPEEvp03YhI6WKOvUTYcSmnZbPw9G3aIVN7DIXYdlqWeQWUh+JKQ60G7riPhBLaTPaw1y3qvPH4PVCV6IeoZ4TScd1XX9t3++xRZgAc1iT2Vj91t9lvBtzPr1PyBlduYyYebArBZiP43Fdov6FnRw0OCW29ZS/2Mrw97aTgHjrrVaoaCHk+lYbRCmzOzaAZlCupMOn0gznms3nEfZfyZRyIp2ezjGhhNWe73g==" SENTRY_DSN="encrypted:BCys3N+cVEYBJIUIVpV/cu+VZkbhdVgDxSFv5SIs7bTfaaDp7kaVVAzTuLlwIXamSltCfzOIwJ3Z7o19ls2f2eR8wX2G/32g1+Z+KnNTaBHexT2eHbrbQ7Hv9zD6rOOFZh4W91xo/wb21IvfEn55QwhZ8N4AFB9qGoAy70ej6WJdKYric9LVwhYbf4LU76MwFvK+8+TyjOt7mPDr8Fz8PyLsJrIuOZtvje5zYg/OPOfeqeKMnw==" +COINGECKO_API_KEY="encrypted:BOnTS+pjMioUn3cROrIDZ4glP+q9hfIBP082qZIePiKQgraXHYnZXPgLq4hb2qqrDrJc55SRQTu/NFWdVf+pqstUz1vDkjKbogIvwbZUsP4ltsn+De2STeERbpYvyu5yAJY+uajL97P4ceqgpeaqaFAMPqZdWUCrKf6tnQ==" diff --git a/apps/server/package.json b/apps/server/package.json index aaa4b685..45305bd5 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -10,7 +10,7 @@ "deploy": "fly deploy --remote-only" }, "dependencies": { - "@dotenvx/dotenvx": "1.14.2", + "@dotenvx/dotenvx": "1.15.0", "@libsql/client": "0.8.0", "@sentry/node": "8.33.1", "@stacks/blockchain-api-client": "8.0.3", diff --git a/apps/server/src/api/tokens/markets.get.ts b/apps/server/src/api/tokens/markets.get.ts new file mode 100644 index 00000000..3fd84136 --- /dev/null +++ b/apps/server/src/api/tokens/markets.get.ts @@ -0,0 +1,36 @@ +import { env } from "~/env"; +import { apiCacheConfig } from "~/lib/api"; + +type CoingeckoCoinsMarketsResponse = { + id: string; + symbol: string; + name: string; + image: string; + current_price: number; + market_cap: number; + price_change_percentage_24h: number; +}[]; + +type TokensMarketsRouteResponse = CoingeckoCoinsMarketsResponse; + +export default defineCachedEventHandler( + async () => { + const url = + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&category=stacks-ecosystem"; + + const data: CoingeckoCoinsMarketsResponse[] = await fetch(url, { + method: "GET", + headers: { + accept: "application/json", + "x-cg-demo-api-key": env.COINGECKO_API_KEY, + }, + }).then((res) => res.json()); + + return data; + }, + { + ...apiCacheConfig, + // Cache for 24 hours + maxAge: 60 * 60 * 24, + }, +); diff --git a/apps/server/src/api/tokens/resolve.get.ts b/apps/server/src/api/tokens/resolve.get.ts new file mode 100644 index 00000000..1c1f14db --- /dev/null +++ b/apps/server/src/api/tokens/resolve.get.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { env } from "~/env"; +import { apiCacheConfig } from "~/lib/api"; +import { getValidatedQueryZod } from "~/lib/nitro"; + +type CoingeckoCoinsIdResponse = { + id: string; + symbol: string; + name: string; + contract_address: string; +}; + +type TokensResolveRouteResponse = CoingeckoCoinsIdResponse; + +const tokensResolveRouteSchema = z.object({ + id: z.string(), +}); + +/** + * Resolves a coingecko token id to a deployed token contract address + */ +export default defineCachedEventHandler( + async (event) => { + const query = await getValidatedQueryZod(event, tokensResolveRouteSchema); + + const url = `https://api.coingecko.com/api/v3/coins/${query.id}`; + + const data: CoingeckoCoinsIdResponse = await fetch(url, { + method: "GET", + headers: { + accept: "application/json", + "x-cg-demo-api-key": env.COINGECKO_API_KEY, + }, + }).then((res) => res.json()); + + return { + id: data.id, + symbol: data.symbol, + name: data.name, + contract_address: data.contract_address, + }; + }, + { + ...apiCacheConfig, + // Cache for 3 days + maxAge: 60 * 60 * 24 * 3, + }, +); diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts index caa6a17c..2700822d 100644 --- a/apps/server/src/env.ts +++ b/apps/server/src/env.ts @@ -9,6 +9,7 @@ export const env = createEnv({ TURSO_DATABASE_URL: z.string().url(), TURSO_AUTH_TOKEN: z.string().optional(), SENTRY_DSN: z.string().url().optional(), + COINGECKO_API_KEY: z.string(), }, runtimeEnv: process.env, diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 16d11ec0..4bbd452f 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -9,6 +9,10 @@ const nextConfig = { protocol: "https", hostname: "**.hiro.so", }, + { + protocol: "https", + hostname: "**.coingecko.com", + }, ], }, }; diff --git a/apps/web/package.json b/apps/web/package.json index 195b0803..9e0956b1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,7 +10,7 @@ "deploy": "fly deploy --remote-only" }, "dependencies": { - "@dotenvx/dotenvx": "1.14.2", + "@dotenvx/dotenvx": "1.15.0", "@hirosystems/token-metadata-api-client": "1.3.0", "@radix-ui/themes": "3.0.5", "@sentry/nextjs": "8.33.1", diff --git a/apps/web/src/app/about/page.tsx b/apps/web/src/app/about/page.tsx index 32a534d4..a13272c7 100644 --- a/apps/web/src/app/about/page.tsx +++ b/apps/web/src/app/about/page.tsx @@ -11,11 +11,9 @@ export const metadata: Metadata = { export default async function AboutPage() { return ( -
- - stackpulse - -
+ + stackpulse +
@@ -39,8 +37,25 @@ export default async function AboutPage() {
+
+ + Data + + + Data is extracted from a self-hosted Stacks blockchain node and + aggregated using PostgreSQL. + + + Token prices and volume is provided by{" "} + + CoinGecko + + . + +
+
- + Open Stats