From e9a2c65179c662241912430028bdfe9a3553ba1e Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Thu, 23 Apr 2026 16:51:39 +0700 Subject: [PATCH 1/8] docs: add fast-track Dune Analytics integration guide --- .../guides/dune-integration.mdx | 346 ++++++++++++++++++ docs.json | 5 +- 2 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 developers/developer-guides/guides/dune-integration.mdx diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx new file mode 100644 index 00000000..9d9a91d4 --- /dev/null +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -0,0 +1,346 @@ +--- +title: Integrating Dune Analytics +description: + Build analytics-driven Initia applications by bridging onchain state with Dune's historical data. +--- + +## Overview + +While live contract reads tell you what's happening *right now*, analytics provide the context of your application's growth and health. Integrating Dune Analytics allows you to provide users with historical insights, network-wide benchmarks, and aggregate metrics. + + + + Handles wallet connections, onchain transactions, and the primary UI. + + + A lightweight proxy that securely manages your private Dune API key. + + + The SQL engine that processes historical data and serves API results. + + + +## Prerequisites + +Ensure you have the following before starting: + +- **Dune API Key:** Obtained from your [Dune settings](https://dune.com/settings/profile) (select **API keys** from the sidebar). +- **Saved Query IDs:** The numeric IDs for the queries you want to display. Open [Dune Queries](https://dune.com/queries) to create or inspect them. +- **Node.js Environment:** To run the backend proxy and frontend application. + +## Step 1: Backend Implementation + +Your backend acts as a secure proxy, ensuring your `DUNE_API_KEY` is never exposed to the frontend. + + + **Pattern:** The backend proxy pattern is language-agnostic. The examples here use Node.js and Express, but the same security model applies in Python, Go, Rust, or any server-side stack. + + +### 1. Setup and Configuration + +Create an `api/` directory, a `src/` folder inside it, and install the required dependencies: + +The backend example uses `express` for routing, `cors` for origin control, and `dotenv` for environment variables. + +```bash +mkdir api && cd api +mkdir src +npm init -y +npm install express cors dotenv +npm pkg set type="module" +``` + +Update `api/package.json` with these scripts: + +```json api/package.json +{ + "type": "module", + "scripts": { + "dev": "node --watch src/server.js", + "start": "node src/server.js" + } +} +``` + +Create `api/.env` and store your secrets in it: + +```env api/.env +PORT=4000 +DUNE_API_KEY=YOUR_DUNE_API_KEY +# Use the numeric IDs from your Dune dashboard. +DUNE_ALLOWED_QUERY_IDS=1234567,2345678,3456789 +FRONTEND_ORIGIN=http://localhost:5173 +``` + +### 2. Proxy Server + +This Express server validates requests and proxies them to Dune. It keeps `DUNE_API_KEY` server-side and only forwards allowed query IDs. + +```js api/src/server.js +import 'dotenv/config' +import cors from 'cors' +import express from 'express' + +const app = express() +const port = Number(process.env.PORT ?? 4000) +const duneApiKey = process.env.DUNE_API_KEY +const frontendOrigin = process.env.FRONTEND_ORIGIN ?? 'http://localhost:5173' +const allowedQueryIds = new Set( + (process.env.DUNE_ALLOWED_QUERY_IDS ?? '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean), +) + +app.use(cors({ origin: frontendOrigin })) +// Kept in place so the server is ready for JSON routes later. +app.use(express.json()) + +app.get('/api/health', (_req, res) => { + res.json({ + ok: true, + duneConfigured: Boolean(duneApiKey), + allowedQueryIds: [...allowedQueryIds], + }) +}) + +app.get('/api/dune/query/:queryId/results', async (req, res) => { + if (!duneApiKey) { + res.status(500).json({ + error: 'DUNE_API_KEY is not configured on the backend.', + }) + return + } + + const { queryId } = req.params + const limit = String(req.query.limit ?? '8') + + if (!/^\d+$/.test(queryId)) { + res.status(400).json({ error: 'Query ID must be numeric.' }) + return + } + + if (allowedQueryIds.size > 0 && !allowedQueryIds.has(queryId)) { + res.status(403).json({ + error: `Query ${queryId} is not allowed by this backend.`, + }) + return + } + + if (!/^\d+$/.test(limit)) { + res.status(400).json({ error: 'limit must be numeric.' }) + return + } + + try { + const duneResponse = await fetch( + `https://api.dune.com/api/v1/query/${queryId}/results?limit=${limit}`, + { + headers: { + 'x-dune-api-key': duneApiKey, + }, + }, + ) + + const bodyText = await duneResponse.text() + res + .status(duneResponse.status) + .type(duneResponse.headers.get('content-type') ?? 'application/json') + .send(bodyText) + } catch (error) { + res.status(502).json({ + error: error instanceof Error ? error.message : 'Failed to fetch Dune results.', + }) + } +}) + +app.listen(port, () => { + console.log(`Dune Radar API listening on http://localhost:${port}`) +}) +``` + +## Step 2: Frontend Implementation + +The frontend connects to your backend proxy, not to Dune directly. + +### 1. Configuration + +Switch to your frontend directory before setting environment variables: + +```bash +cd ../frontend +``` + +Add your backend's base URL and query IDs to your public environment variables (e.g., `.env`): + +```env frontend/.env +VITE_DUNE_API_BASE_URL=http://localhost:4000/api +VITE_DUNE_QUERY_ID_OVERVIEW=1234567 +VITE_DUNE_QUERY_ID_BRIDGES=2345678 +VITE_DUNE_QUERY_ID_WALLETS=3456789 +``` + +### 2. Fetch Helper + +Use this helper to abstract the backend API call: + +```ts frontend/src/lib/dune.ts +const DUNE_API_BASE_URL = import.meta.env.VITE_DUNE_API_BASE_URL ?? 'http://localhost:4000/api' + +export async function fetchLatestDuneResult(queryId: string, limit = 8) { + if (!queryId) { + throw new Error('Missing Dune query ID. Update the frontend .env with your Dune query IDs.') + } + + const response = await fetch(`${DUNE_API_BASE_URL}/dune/query/${queryId}/results?limit=${limit}`) + if (!response.ok) { + const text = await response.text() + throw new Error(text || `Dune request failed with status ${response.status}`) + } + return response.json() +} +``` + + + **Limit:** The backend proxy supports `?limit=` on the results endpoint, and the frontend helper exposes it as the optional `limit` argument. + + +## Step 3: Mapping Multiple Queries + +Use stable **Application Keys** to map your UI components to specific Dune Query IDs. This allows you to update queries on the backend without changing frontend code. + +```ts frontend/src/lib/dune.ts +export const DUNE_QUERY_MAP = { + overview: { + title: 'Network Overview', + description: 'A high-level summary of Initia transaction activity and chain health.', + queryId: import.meta.env.VITE_DUNE_QUERY_ID_OVERVIEW, + }, + bridges: { + title: 'Bridge Routes', + description: 'Cross-chain transfer flow across Initia bridge routes and assets.', + queryId: import.meta.env.VITE_DUNE_QUERY_ID_BRIDGES, + }, + wallets: { + title: 'Message Activity', + description: 'Readable message activity showing what kinds of actions wallets are taking.', + queryId: import.meta.env.VITE_DUNE_QUERY_ID_WALLETS, + }, +} + +export const DUNE_QUERY_OPTIONS = Object.entries(DUNE_QUERY_MAP).map(([key, value]) => ({ + key, + ...value, +})) + +export function getQueryConfig(queryKey) { + return DUNE_QUERY_MAP[queryKey] ?? DUNE_QUERY_MAP.overview +} +``` + +### 3. Usage Example + +After defining `DUNE_QUERY_MAP`, a React component can use the helper like this: + +```ts frontend/src/components/Analytics.tsx +import { useEffect, useState } from 'react' +import { DUNE_QUERY_MAP, fetchLatestDuneResult } from '../lib/dune' + +export function Analytics() { + const [data, setData] = useState([]) + const [error, setError] = useState('') + + useEffect(() => { + fetchLatestDuneResult(DUNE_QUERY_MAP.overview.queryId) + .then((result) => setData(result.result.rows)) + .catch((err) => setError(err.message)) + }, []) + + if (error) return

{error}

+ + return
{JSON.stringify(data, null, 2)}
+} +``` + +## Step 4: Local Development + + + + Note the numeric IDs for your saved queries in the Dune dashboard. + + + Run your proxy server from the `api/` directory with `npm run dev`. + + + Run the frontend app from the `frontend/` directory with `npm run dev` and + verify `VITE_DUNE_API_BASE_URL` points to the backend. + + + Confirm your proxy is live by visiting `http://localhost:4000/api/health` in + your browser or running: + ```bash + curl http://localhost:4000/api/health + ``` + + + Launch your app and refresh a query to confirm the frontend renders data and the backend logs successful requests. + + + +## Troubleshooting and Security Tips + +- **API Key Security:** **Never** expose `DUNE_API_KEY` in your frontend. It must remain server-side. +- **Source of Truth:** Dune is an analytics layer, not your contract's source of truth. Keep critical validation and state transitions onchain. +- **Access Control:** Always use the `DUNE_ALLOWED_QUERY_IDS` allowlist on your backend to prevent unauthorized proxying. +- **Data Freshness:** Dune's results endpoint returns the latest saved execution, not necessarily a fresh one. +- **Maintainability:** Use Application Keys (like `overview`) in your UI components instead of hardcoding numeric IDs. + +## Advanced: Saving Preferences Onchain (Optional) + +For personalized experiences, you can store a user's selected analytics view on the Initia rollup. + +```solidity +struct SavedView { + uint256 id; + address owner; + string viewKey; // e.g., "overview" + uint64 createdAt; + bool archived; +} +``` + + + **Performance:** Store only the **preference** (the `view key`) onchain. The + actual analytics data should always be fetched from Dune via your backend. + + +## Resources + +### Common Dune Example Tables + +These are example tables frequently used for Initia analytics: + +| Table | Recommended Use | +|---|---| +| `initia.transactions` | Network activity and gas trends. | +| `initia.bridge_transfers` | Asset flow and bridge route analytics. | +| `initia.tx_messages` | Detailed message-type activity. | + +### Choosing the Right Tool + +| Feature | [Initia Indexer](/api-reference/rollup-indexer/introduction) | Dune Analytics | +|---|---|---| +| **Data Scope** | Real-time app state. | Historical trends & aggregates. | +| **Query Style** | Specific accounts/events. | Complex SQL across millions of rows. | +| **Primary Use** | Core app logic. | Dashboards & public reports. | + +## Next Steps + + + + Reduce API consumption by caching results on your server. + + + Combine results from multiple Dune views into a single UI. + + diff --git a/docs.json b/docs.json index 25d6bf8c..9ea19fdb 100644 --- a/docs.json +++ b/docs.json @@ -202,7 +202,10 @@ }, { "group": "Quick Guides", - "pages": ["developers/developer-guides/guides/exchange-integration"] + "pages": [ + "developers/developer-guides/guides/exchange-integration", + "developers/developer-guides/guides/dune-integration" + ] }, { "group": "VM Specific Tutorials", From d3fe0ee1a17ba4a518ecc24db4af5cdbd05c5eca Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Thu, 23 Apr 2026 16:57:09 +0700 Subject: [PATCH 2/8] style: format with Prettier --- .../guides/dune-integration.mdx | 124 +++++++++++------- 1 file changed, 80 insertions(+), 44 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index 9d9a91d4..e0f00d69 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -1,12 +1,16 @@ --- title: Integrating Dune Analytics description: - Build analytics-driven Initia applications by bridging onchain state with Dune's historical data. + Build analytics-driven Initia applications by bridging onchain state with + Dune's historical data. --- ## Overview -While live contract reads tell you what's happening *right now*, analytics provide the context of your application's growth and health. Integrating Dune Analytics allows you to provide users with historical insights, network-wide benchmarks, and aggregate metrics. +While live contract reads tell you what's happening _right now_, analytics +provide the context of your application's growth and health. Integrating Dune +Analytics allows you to provide users with historical insights, network-wide +benchmarks, and aggregate metrics. @@ -24,23 +28,31 @@ While live contract reads tell you what's happening *right now*, analytics provi Ensure you have the following before starting: -- **Dune API Key:** Obtained from your [Dune settings](https://dune.com/settings/profile) (select **API keys** from the sidebar). -- **Saved Query IDs:** The numeric IDs for the queries you want to display. Open [Dune Queries](https://dune.com/queries) to create or inspect them. +- **Dune API Key:** Obtained from your + [Dune settings](https://dune.com/settings/profile) (select **API keys** from + the sidebar). +- **Saved Query IDs:** The numeric IDs for the queries you want to display. Open + [Dune Queries](https://dune.com/queries) to create or inspect them. - **Node.js Environment:** To run the backend proxy and frontend application. ## Step 1: Backend Implementation -Your backend acts as a secure proxy, ensuring your `DUNE_API_KEY` is never exposed to the frontend. +Your backend acts as a secure proxy, ensuring your `DUNE_API_KEY` is never +exposed to the frontend. - **Pattern:** The backend proxy pattern is language-agnostic. The examples here use Node.js and Express, but the same security model applies in Python, Go, Rust, or any server-side stack. + **Pattern:** The backend proxy pattern is language-agnostic. The examples here + use Node.js and Express, but the same security model applies in Python, Go, + Rust, or any server-side stack. ### 1. Setup and Configuration -Create an `api/` directory, a `src/` folder inside it, and install the required dependencies: +Create an `api/` directory, a `src/` folder inside it, and install the required +dependencies: -The backend example uses `express` for routing, `cors` for origin control, and `dotenv` for environment variables. +The backend example uses `express` for routing, `cors` for origin control, and +`dotenv` for environment variables. ```bash mkdir api && cd api @@ -74,7 +86,8 @@ FRONTEND_ORIGIN=http://localhost:5173 ### 2. Proxy Server -This Express server validates requests and proxies them to Dune. It keeps `DUNE_API_KEY` server-side and only forwards allowed query IDs. +This Express server validates requests and proxies them to Dune. It keeps +`DUNE_API_KEY` server-side and only forwards allowed query IDs. ```js api/src/server.js import 'dotenv/config' @@ -149,7 +162,10 @@ app.get('/api/dune/query/:queryId/results', async (req, res) => { .send(bodyText) } catch (error) { res.status(502).json({ - error: error instanceof Error ? error.message : 'Failed to fetch Dune results.', + error: + error instanceof Error + ? error.message + : 'Failed to fetch Dune results.', }) } }) @@ -171,7 +187,8 @@ Switch to your frontend directory before setting environment variables: cd ../frontend ``` -Add your backend's base URL and query IDs to your public environment variables (e.g., `.env`): +Add your backend's base URL and query IDs to your public environment variables +(e.g., `.env`): ```env frontend/.env VITE_DUNE_API_BASE_URL=http://localhost:4000/api @@ -185,53 +202,68 @@ VITE_DUNE_QUERY_ID_WALLETS=3456789 Use this helper to abstract the backend API call: ```ts frontend/src/lib/dune.ts -const DUNE_API_BASE_URL = import.meta.env.VITE_DUNE_API_BASE_URL ?? 'http://localhost:4000/api' +const DUNE_API_BASE_URL = + import.meta.env.VITE_DUNE_API_BASE_URL ?? 'http://localhost:4000/api' export async function fetchLatestDuneResult(queryId: string, limit = 8) { if (!queryId) { - throw new Error('Missing Dune query ID. Update the frontend .env with your Dune query IDs.') + throw new Error( + 'Missing Dune query ID. Update the frontend .env with your Dune query IDs.', + ) } - const response = await fetch(`${DUNE_API_BASE_URL}/dune/query/${queryId}/results?limit=${limit}`) + const response = await fetch( + `${DUNE_API_BASE_URL}/dune/query/${queryId}/results?limit=${limit}`, + ) if (!response.ok) { const text = await response.text() - throw new Error(text || `Dune request failed with status ${response.status}`) + throw new Error( + text || `Dune request failed with status ${response.status}`, + ) } return response.json() } ``` - **Limit:** The backend proxy supports `?limit=` on the results endpoint, and the frontend helper exposes it as the optional `limit` argument. + **Limit:** The backend proxy supports `?limit=` on the results endpoint, and + the frontend helper exposes it as the optional `limit` argument. ## Step 3: Mapping Multiple Queries -Use stable **Application Keys** to map your UI components to specific Dune Query IDs. This allows you to update queries on the backend without changing frontend code. +Use stable **Application Keys** to map your UI components to specific Dune Query +IDs. This allows you to update queries on the backend without changing frontend +code. ```ts frontend/src/lib/dune.ts export const DUNE_QUERY_MAP = { overview: { title: 'Network Overview', - description: 'A high-level summary of Initia transaction activity and chain health.', + description: + 'A high-level summary of Initia transaction activity and chain health.', queryId: import.meta.env.VITE_DUNE_QUERY_ID_OVERVIEW, }, bridges: { title: 'Bridge Routes', - description: 'Cross-chain transfer flow across Initia bridge routes and assets.', + description: + 'Cross-chain transfer flow across Initia bridge routes and assets.', queryId: import.meta.env.VITE_DUNE_QUERY_ID_BRIDGES, }, wallets: { title: 'Message Activity', - description: 'Readable message activity showing what kinds of actions wallets are taking.', + description: + 'Readable message activity showing what kinds of actions wallets are taking.', queryId: import.meta.env.VITE_DUNE_QUERY_ID_WALLETS, }, } -export const DUNE_QUERY_OPTIONS = Object.entries(DUNE_QUERY_MAP).map(([key, value]) => ({ - key, - ...value, -})) +export const DUNE_QUERY_OPTIONS = Object.entries(DUNE_QUERY_MAP).map( + ([key, value]) => ({ + key, + ...value, + }), +) export function getQueryConfig(queryKey) { return DUNE_QUERY_MAP[queryKey] ?? DUNE_QUERY_MAP.overview @@ -277,27 +309,31 @@ export function Analytics() { Confirm your proxy is live by visiting `http://localhost:4000/api/health` in - your browser or running: - ```bash - curl http://localhost:4000/api/health - ``` + your browser or running: ```bash curl http://localhost:4000/api/health ``` - Launch your app and refresh a query to confirm the frontend renders data and the backend logs successful requests. + Launch your app and refresh a query to confirm the frontend renders data and + the backend logs successful requests. ## Troubleshooting and Security Tips -- **API Key Security:** **Never** expose `DUNE_API_KEY` in your frontend. It must remain server-side. -- **Source of Truth:** Dune is an analytics layer, not your contract's source of truth. Keep critical validation and state transitions onchain. -- **Access Control:** Always use the `DUNE_ALLOWED_QUERY_IDS` allowlist on your backend to prevent unauthorized proxying. -- **Data Freshness:** Dune's results endpoint returns the latest saved execution, not necessarily a fresh one. -- **Maintainability:** Use Application Keys (like `overview`) in your UI components instead of hardcoding numeric IDs. +- **API Key Security:** **Never** expose `DUNE_API_KEY` in your frontend. It + must remain server-side. +- **Source of Truth:** Dune is an analytics layer, not your contract's source of + truth. Keep critical validation and state transitions onchain. +- **Access Control:** Always use the `DUNE_ALLOWED_QUERY_IDS` allowlist on your + backend to prevent unauthorized proxying. +- **Data Freshness:** Dune's results endpoint returns the latest saved + execution, not necessarily a fresh one. +- **Maintainability:** Use Application Keys (like `overview`) in your UI + components instead of hardcoding numeric IDs. ## Advanced: Saving Preferences Onchain (Optional) -For personalized experiences, you can store a user's selected analytics view on the Initia rollup. +For personalized experiences, you can store a user's selected analytics view on +the Initia rollup. ```solidity struct SavedView { @@ -320,19 +356,19 @@ struct SavedView { These are example tables frequently used for Initia analytics: -| Table | Recommended Use | -|---|---| -| `initia.transactions` | Network activity and gas trends. | +| Table | Recommended Use | +| ------------------------- | -------------------------------------- | +| `initia.transactions` | Network activity and gas trends. | | `initia.bridge_transfers` | Asset flow and bridge route analytics. | -| `initia.tx_messages` | Detailed message-type activity. | +| `initia.tx_messages` | Detailed message-type activity. | ### Choosing the Right Tool -| Feature | [Initia Indexer](/api-reference/rollup-indexer/introduction) | Dune Analytics | -|---|---|---| -| **Data Scope** | Real-time app state. | Historical trends & aggregates. | -| **Query Style** | Specific accounts/events. | Complex SQL across millions of rows. | -| **Primary Use** | Core app logic. | Dashboards & public reports. | +| Feature | [Initia Indexer](/api-reference/rollup-indexer/introduction) | Dune Analytics | +| --------------- | ------------------------------------------------------------ | ------------------------------------ | +| **Data Scope** | Real-time app state. | Historical trends & aggregates. | +| **Query Style** | Specific accounts/events. | Complex SQL across millions of rows. | +| **Primary Use** | Core app logic. | Dashboards & public reports. | ## Next Steps From c3f791acd12eb40fafa6bc5a726c375ab0329ae6 Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Wed, 6 May 2026 14:20:36 +0700 Subject: [PATCH 3/8] docs: refine Dune integration guide structure and examples --- .../guides/dune-integration.mdx | 451 +++++++++++------- 1 file changed, 280 insertions(+), 171 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index e0f00d69..8d6cf05e 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -7,64 +7,90 @@ description: ## Overview -While live contract reads tell you what's happening _right now_, analytics -provide the context of your application's growth and health. Integrating Dune -Analytics allows you to provide users with historical insights, network-wide -benchmarks, and aggregate metrics. +Dune is an onchain analytics platform for exploring historical blockchain data, +building SQL queries, and turning the results into dashboards or API-backed +application views. While live contract reads tell you what's happening _right +now_, Dune adds the historical context that helps you explain growth, usage, +and network health. + +### The End-State + +By the end of this guide, you will have a functional **Onchain Saved Views** +implementation where: +1. **Users** save their preferred analytics view (e.g., Bridge Volume) to a + smart contract on an Initia rollup. +2. **The Frontend** reads that onchain preference and fetches matching + historical data from Dune via a secure proxy. +3. **The Developer** maintains full control over API keys and allowed queries + without exposing secrets to the browser. - + - Handles wallet connections, onchain transactions, and the primary UI. + The UI built with InterwovenKit for wallet connection and onchain actions. + + + A VM-specific contract or module that stores user-specific analytics + preferences. - A lightweight proxy that securely manages your private Dune API key. + A lightweight proxy that manages your `DUNE_API_KEY` and filters requests. - The SQL engine that processes historical data and serves API results. + The SQL engine that processes historical data and serves indexed results. -## Prerequisites +## Dune App Capabilities -Ensure you have the following before starting: +If you want to work directly in Dune before wiring anything into an Initia app, +the web app gives you the core building blocks you need: query editing, +dashboards, search/discovery, and scheduled refreshes. -- **Dune API Key:** Obtained from your - [Dune settings](https://dune.com/settings/profile) (select **API keys** from - the sidebar). -- **Saved Query IDs:** The numeric IDs for the queries you want to display. Open - [Dune Queries](https://dune.com/queries) to create or inspect them. + + Use Dune directly to prototype queries and dashboards. Use the API when you + want to embed saved results into an Initia app or backend flow. + + +## Prerequisites + +- **Dune API Key:** Obtained from your [Dune settings](https://dune.com/settings/profile). +- **Saved Query IDs:** The numeric IDs for the queries you want to display in your app. - **Node.js Environment:** To run the backend proxy and frontend application. +- **Foundry:** To deploy the preference registry contract to your Initia rollup. +- **Registry Contract Address:** The deployed address for your saved-view contract. ## Step 1: Backend Implementation Your backend acts as a secure proxy, ensuring your `DUNE_API_KEY` is never exposed to the frontend. - - **Pattern:** The backend proxy pattern is language-agnostic. The examples here - use Node.js and Express, but the same security model applies in Python, Go, - Rust, or any server-side stack. - + + Never expose `DUNE_API_KEY` in client-side code. Keep the key on the backend + and proxy all Dune requests through your server. + ### 1. Setup and Configuration -Create an `api/` directory, a `src/` folder inside it, and install the required -dependencies: - -The backend example uses `express` for routing, `cors` for origin control, and -`dotenv` for environment variables. +Create an `api/` directory and install the required dependencies: ```bash mkdir api && cd api -mkdir src npm init -y npm install express cors dotenv -npm pkg set type="module" +``` + +Create `api/.env` and store your backend configuration there: + +```env api/.env +PORT=4000 +DUNE_API_KEY=YOUR_DUNE_API_KEY +DUNE_ALLOWED_QUERY_IDS=1234567,2345678,3456789 +FRONTEND_ORIGIN=http://localhost:5173 ``` Update `api/package.json` with these scripts: -```json api/package.json +```json { "type": "module", "scripts": { @@ -74,20 +100,9 @@ Update `api/package.json` with these scripts: } ``` -Create `api/.env` and store your secrets in it: - -```env api/.env -PORT=4000 -DUNE_API_KEY=YOUR_DUNE_API_KEY -# Use the numeric IDs from your Dune dashboard. -DUNE_ALLOWED_QUERY_IDS=1234567,2345678,3456789 -FRONTEND_ORIGIN=http://localhost:5173 -``` - ### 2. Proxy Server -This Express server validates requests and proxies them to Dune. It keeps -`DUNE_API_KEY` server-side and only forwards allowed query IDs. +This server validates requests and only forwards allowed query IDs to Dune. ```js api/src/server.js import 'dotenv/config' @@ -97,7 +112,8 @@ import express from 'express' const app = express() const port = Number(process.env.PORT ?? 4000) const duneApiKey = process.env.DUNE_API_KEY -const frontendOrigin = process.env.FRONTEND_ORIGIN ?? 'http://localhost:5173' +const frontendOrigin = + process.env.FRONTEND_ORIGIN ?? 'http://localhost:5173' const allowedQueryIds = new Set( (process.env.DUNE_ALLOWED_QUERY_IDS ?? '') .split(',') @@ -106,7 +122,6 @@ const allowedQueryIds = new Set( ) app.use(cors({ origin: frontendOrigin })) -// Kept in place so the server is ready for JSON routes later. app.use(express.json()) app.get('/api/health', (_req, res) => { @@ -118,16 +133,14 @@ app.get('/api/health', (_req, res) => { }) app.get('/api/dune/query/:queryId/results', async (req, res) => { + const { queryId } = req.params + const limit = String(req.query.limit ?? '8') + if (!duneApiKey) { - res.status(500).json({ - error: 'DUNE_API_KEY is not configured on the backend.', - }) + res.status(500).json({ error: 'DUNE_API_KEY is not configured on the backend.' }) return } - const { queryId } = req.params - const limit = String(req.query.limit ?? '8') - if (!/^\d+$/.test(queryId)) { res.status(400).json({ error: 'Query ID must be numeric.' }) return @@ -154,7 +167,6 @@ app.get('/api/dune/query/:queryId/results', async (req, res) => { }, }, ) - const bodyText = await duneResponse.text() res .status(duneResponse.status) @@ -171,72 +183,76 @@ app.get('/api/dune/query/:queryId/results', async (req, res) => { }) app.listen(port, () => { - console.log(`Dune Radar API listening on http://localhost:${port}`) + console.log(`Dune Radar API listening on port ${port}`) }) ``` -## Step 2: Frontend Implementation - -The frontend connects to your backend proxy, not to Dune directly. +## Step 2: Onchain Registry -### 1. Configuration +This example uses a MiniEVM Solidity contract to store user view preferences. +The same registry pattern can be implemented on MiniMove or MiniWasm chains +with the matching Move or CosmWasm contract/module instead. -Switch to your frontend directory before setting environment variables: + + The registry example is MiniEVM-specific, but the saved-view pattern itself + is VM-agnostic. Use the equivalent contract/module syntax for MiniMove or + MiniWasm if your rollup uses a different VM. + -```bash -cd ../frontend +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract DuneRadarRegistry { + struct SavedView { + uint256 id; + address owner; + string viewKey; // e.g., "overview", "bridges" + uint64 createdAt; + bool archived; + } + + uint256 private _nextId = 1; + mapping(uint256 => SavedView) private _savedViews; + mapping(address => uint256[]) private _ownerViews; + + function createSavedView(string calldata viewKey) external returns (uint256) { + uint256 id = _nextId++; + _savedViews[id] = SavedView(id, msg.sender, viewKey, uint64(block.timestamp), false); + _ownerViews[msg.sender].push(id); + return id; + } + + function archiveSavedView(uint256 savedViewId) external { + SavedView storage savedView = _savedViews[savedViewId]; + require(savedView.owner == msg.sender, "Not owner"); + savedView.archived = true; + } + + function getSavedViews(address owner) external view returns (SavedView[] memory) { + uint256[] storage ids = _ownerViews[owner]; + SavedView[] memory results = new SavedView[](ids.length); + for (uint256 i = 0; i < ids.length; i++) { + results[i] = _savedViews[ids[i]]; + } + return results; + } +} ``` -Add your backend's base URL and query IDs to your public environment variables -(e.g., `.env`): +## Step 3: Frontend Implementation -```env frontend/.env -VITE_DUNE_API_BASE_URL=http://localhost:4000/api -VITE_DUNE_QUERY_ID_OVERVIEW=1234567 -VITE_DUNE_QUERY_ID_BRIDGES=2345678 -VITE_DUNE_QUERY_ID_WALLETS=3456789 -``` +The frontend uses **InterwovenKit** to manage onchain preferences and the +proxy for fetching historical data. -### 2. Fetch Helper +### 1. Dune Helper -Use this helper to abstract the backend API call: +Map stable application keys to your numeric Dune query IDs. ```ts frontend/src/lib/dune.ts const DUNE_API_BASE_URL = import.meta.env.VITE_DUNE_API_BASE_URL ?? 'http://localhost:4000/api' -export async function fetchLatestDuneResult(queryId: string, limit = 8) { - if (!queryId) { - throw new Error( - 'Missing Dune query ID. Update the frontend .env with your Dune query IDs.', - ) - } - - const response = await fetch( - `${DUNE_API_BASE_URL}/dune/query/${queryId}/results?limit=${limit}`, - ) - if (!response.ok) { - const text = await response.text() - throw new Error( - text || `Dune request failed with status ${response.status}`, - ) - } - return response.json() -} -``` - - - **Limit:** The backend proxy supports `?limit=` on the results endpoint, and - the frontend helper exposes it as the optional `limit` argument. - - -## Step 3: Mapping Multiple Queries - -Use stable **Application Keys** to map your UI components to specific Dune Query -IDs. This allows you to update queries on the backend without changing frontend -code. - -```ts frontend/src/lib/dune.ts export const DUNE_QUERY_MAP = { overview: { title: 'Network Overview', @@ -268,94 +284,198 @@ export const DUNE_QUERY_OPTIONS = Object.entries(DUNE_QUERY_MAP).map( export function getQueryConfig(queryKey) { return DUNE_QUERY_MAP[queryKey] ?? DUNE_QUERY_MAP.overview } + +export async function fetchLatestDuneResult(queryId: string, limit = 8) { + if (!queryId) { + throw new Error( + 'Missing Dune query ID. Update the frontend .env with your Dune query IDs.', + ) + } + + const response = await fetch( + `${DUNE_API_BASE_URL}/dune/query/${queryId}/results?limit=${limit}`, + ) + if (!response.ok) { + const text = await response.text() + throw new Error(text || `Dune request failed with status ${response.status}`) + } + return response.json() +} +``` + +```env frontend/.env +VITE_DUNE_API_BASE_URL=http://localhost:4000/api +VITE_DUNE_QUERY_ID_OVERVIEW=1234567 +VITE_DUNE_QUERY_ID_BRIDGES=2345678 +VITE_DUNE_QUERY_ID_WALLETS=3456789 +VITE_APPCHAIN_ID=your-appchain-id +VITE_REGISTRY_ADDRESS=0xYourDeployedRegistryContractAddress +``` + + + These examples use local-development defaults so you can run the guide + end-to-end on your machine. Replace the URLs with your deployed frontend and + backend origins when you publish the app. + + +### 2. Contract Helper + +This helper encodes the `MsgCall` for your registry contract using `viem` and +keeps the frontend example focused on the transaction payload rather than the +submission plumbing. + +```ts frontend/src/lib/contract.ts +import { encodeFunctionData } from 'viem' + +const CONTRACT_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS +const CHAIN_ID = import.meta.env.VITE_APPCHAIN_ID + +export const REGISTRY_ABI = [{ + name: 'createSavedView', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ name: 'viewKey', type: 'string' }], + outputs: [{ name: 'savedViewId', type: 'uint256' }], +}] + +export function buildCreateSavedViewMessage(initiaAddress: string, viewKey: string) { + const input = encodeFunctionData({ + abi: REGISTRY_ABI, + functionName: 'createSavedView', + args: [viewKey], + }) + + return { + chainId: CHAIN_ID, + messages: [{ + typeUrl: '/minievm.evm.v1.MsgCall', + value: { + sender: initiaAddress.toLowerCase(), + contractAddr: CONTRACT_ADDRESS, + input, + value: '0', + }, + }], + } +} ``` -### 3. Usage Example +### 3. Rendering Results + +Use a reusable table component to format Dune's tabular data. + +```tsx frontend/src/components/DuneTable.tsx +export function DuneTable({ rows }: { rows: any[] }) { + if (!rows?.length) return
No data found for this view.
+ + const columns = Object.keys(rows[0]).slice(0, 5) + + return ( +
+ + + {columns.map(col => )} + + + {rows.map((row, i) => ( + + {columns.map(col => ( + + ))} + + ))} + +
{col}
{String(row[col] ?? '—')}
+
+ ) +} +``` -After defining `DUNE_QUERY_MAP`, a React component can use the helper like this: +### 4. Integration Example -```ts frontend/src/components/Analytics.tsx +Use `useInterwovenKit` to connect wallets and store preferences onchain. + +```tsx frontend/src/App.tsx import { useEffect, useState } from 'react' -import { DUNE_QUERY_MAP, fetchLatestDuneResult } from '../lib/dune' +import { useInterwovenKit } from '@initia/interwovenkit-react' +import { DUNE_QUERY_OPTIONS, fetchLatestDuneResult, getQueryConfig } from './lib/dune' +import { buildCreateSavedViewMessage } from './lib/contract' +import { DuneTable } from './components/DuneTable' export function Analytics() { + const { initiaAddress, requestTxBlock } = useInterwovenKit() + const [viewKey, setViewKey] = useState(DUNE_QUERY_OPTIONS[0]?.key ?? 'overview') const [data, setData] = useState([]) - const [error, setError] = useState('') useEffect(() => { - fetchLatestDuneResult(DUNE_QUERY_MAP.overview.queryId) - .then((result) => setData(result.result.rows)) - .catch((err) => setError(err.message)) - }, []) + void fetchLatestDuneResult(getQueryConfig(viewKey).queryId) + .then((res) => setData(res.result.rows)) + }, [viewKey]) - if (error) return

{error}

+ const handleSaveView = async (key: string) => { + if (!initiaAddress) return + await requestTxBlock(buildCreateSavedViewMessage(initiaAddress, key)) + } - return
{JSON.stringify(data, null, 2)}
+ return ( +
+ + + +
+ ) } ``` -## Step 4: Local Development - - - - Note the numeric IDs for your saved queries in the Dune dashboard. - - - Run your proxy server from the `api/` directory with `npm run dev`. - - - Run the frontend app from the `frontend/` directory with `npm run dev` and - verify `VITE_DUNE_API_BASE_URL` points to the backend. - - - Confirm your proxy is live by visiting `http://localhost:4000/api/health` in - your browser or running: ```bash curl http://localhost:4000/api/health ``` - - - Launch your app and refresh a query to confirm the frontend renders data and - the backend logs successful requests. - - +In a complete implementation, the frontend also loads saved views from the +contract, maps each saved `viewKey` back through `getQueryConfig`, and shows +the matching Dune result for the selected preference. The snippet above keeps +the core flow visible without repeating the full UI. + +## SQL Recipes for Initia + +Use these templates in the Dune Query Editor to build your analytics views. + +### 1. Daily Transaction Volume +```sql +SELECT + date_trunc('day', block_time) as day, + count(*) as tx_count, + count(distinct "from") as unique_users +FROM initia.transactions +WHERE block_time > now() - interval '30 days' +GROUP BY 1 ORDER BY 1 DESC +``` + +### 2. Popular Message Types +```sql +SELECT + type_url, + count(*) as count +FROM initia.tx_messages +GROUP BY 1 ORDER BY 2 DESC +LIMIT 10 +``` ## Troubleshooting and Security Tips - **API Key Security:** **Never** expose `DUNE_API_KEY` in your frontend. It - must remain server-side. -- **Source of Truth:** Dune is an analytics layer, not your contract's source of - truth. Keep critical validation and state transitions onchain. + must remain server-side in your proxy. +- **Source of Truth:** Dune is an analytics layer. Keep critical app logic and + state validation onchain. - **Access Control:** Always use the `DUNE_ALLOWED_QUERY_IDS` allowlist on your backend to prevent unauthorized proxying. - **Data Freshness:** Dune's results endpoint returns the latest saved - execution, not necessarily a fresh one. -- **Maintainability:** Use Application Keys (like `overview`) in your UI - components instead of hardcoding numeric IDs. - -## Advanced: Saving Preferences Onchain (Optional) - -For personalized experiences, you can store a user's selected analytics view on -the Initia rollup. - -```solidity -struct SavedView { - uint256 id; - address owner; - string viewKey; // e.g., "overview" - uint64 createdAt; - bool archived; -} -``` - - - **Performance:** Store only the **preference** (the `view key`) onchain. The - actual analytics data should always be fetched from Dune via your backend. - + execution. For real-time state, use the [Initia Indexer](/api-reference/rollup-indexer/introduction). ## Resources ### Common Dune Example Tables -These are example tables frequently used for Initia analytics: - | Table | Recommended Use | | ------------------------- | -------------------------------------- | | `initia.transactions` | Network activity and gas trends. | @@ -369,14 +489,3 @@ These are example tables frequently used for Initia analytics: | **Data Scope** | Real-time app state. | Historical trends & aggregates. | | **Query Style** | Specific accounts/events. | Complex SQL across millions of rows. | | **Primary Use** | Core app logic. | Dashboards & public reports. | - -## Next Steps - - - - Reduce API consumption by caching results on your server. - - - Combine results from multiple Dune views into a single UI. - - From 93b6e6caebe494b6797a5bcc1d5617675ab27930 Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Wed, 6 May 2026 14:23:01 +0700 Subject: [PATCH 4/8] style: Prettier formatting --- .../guides/dune-integration.mdx | 116 +++++++++++------- 1 file changed, 73 insertions(+), 43 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index 8d6cf05e..4dac2b9d 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -10,13 +10,14 @@ description: Dune is an onchain analytics platform for exploring historical blockchain data, building SQL queries, and turning the results into dashboards or API-backed application views. While live contract reads tell you what's happening _right -now_, Dune adds the historical context that helps you explain growth, usage, -and network health. +now_, Dune adds the historical context that helps you explain growth, usage, and +network health. ### The End-State By the end of this guide, you will have a functional **Onchain Saved Views** implementation where: + 1. **Users** save their preferred analytics view (e.g., Bridge Volume) to a smart contract on an Initia rollup. 2. **The Frontend** reads that onchain preference and fetches matching @@ -53,11 +54,14 @@ dashboards, search/discovery, and scheduled refreshes. ## Prerequisites -- **Dune API Key:** Obtained from your [Dune settings](https://dune.com/settings/profile). -- **Saved Query IDs:** The numeric IDs for the queries you want to display in your app. +- **Dune API Key:** Obtained from your + [Dune settings](https://dune.com/settings/profile). +- **Saved Query IDs:** The numeric IDs for the queries you want to display in + your app. - **Node.js Environment:** To run the backend proxy and frontend application. - **Foundry:** To deploy the preference registry contract to your Initia rollup. -- **Registry Contract Address:** The deployed address for your saved-view contract. +- **Registry Contract Address:** The deployed address for your saved-view + contract. ## Step 1: Backend Implementation @@ -112,8 +116,7 @@ import express from 'express' const app = express() const port = Number(process.env.PORT ?? 4000) const duneApiKey = process.env.DUNE_API_KEY -const frontendOrigin = - process.env.FRONTEND_ORIGIN ?? 'http://localhost:5173' +const frontendOrigin = process.env.FRONTEND_ORIGIN ?? 'http://localhost:5173' const allowedQueryIds = new Set( (process.env.DUNE_ALLOWED_QUERY_IDS ?? '') .split(',') @@ -137,7 +140,9 @@ app.get('/api/dune/query/:queryId/results', async (req, res) => { const limit = String(req.query.limit ?? '8') if (!duneApiKey) { - res.status(500).json({ error: 'DUNE_API_KEY is not configured on the backend.' }) + res + .status(500) + .json({ error: 'DUNE_API_KEY is not configured on the backend.' }) return } @@ -190,12 +195,12 @@ app.listen(port, () => { ## Step 2: Onchain Registry This example uses a MiniEVM Solidity contract to store user view preferences. -The same registry pattern can be implemented on MiniMove or MiniWasm chains -with the matching Move or CosmWasm contract/module instead. +The same registry pattern can be implemented on MiniMove or MiniWasm chains with +the matching Move or CosmWasm contract/module instead. - The registry example is MiniEVM-specific, but the saved-view pattern itself - is VM-agnostic. Use the equivalent contract/module syntax for MiniMove or + The registry example is MiniEVM-specific, but the saved-view pattern itself is + VM-agnostic. Use the equivalent contract/module syntax for MiniMove or MiniWasm if your rollup uses a different VM. @@ -242,8 +247,8 @@ contract DuneRadarRegistry { ## Step 3: Frontend Implementation -The frontend uses **InterwovenKit** to manage onchain preferences and the -proxy for fetching historical data. +The frontend uses **InterwovenKit** to manage onchain preferences and the proxy +for fetching historical data. ### 1. Dune Helper @@ -297,7 +302,9 @@ export async function fetchLatestDuneResult(queryId: string, limit = 8) { ) if (!response.ok) { const text = await response.text() - throw new Error(text || `Dune request failed with status ${response.status}`) + throw new Error( + text || `Dune request failed with status ${response.status}`, + ) } return response.json() } @@ -330,15 +337,20 @@ import { encodeFunctionData } from 'viem' const CONTRACT_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS const CHAIN_ID = import.meta.env.VITE_APPCHAIN_ID -export const REGISTRY_ABI = [{ - name: 'createSavedView', - type: 'function', - stateMutability: 'nonpayable', - inputs: [{ name: 'viewKey', type: 'string' }], - outputs: [{ name: 'savedViewId', type: 'uint256' }], -}] +export const REGISTRY_ABI = [ + { + name: 'createSavedView', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ name: 'viewKey', type: 'string' }], + outputs: [{ name: 'savedViewId', type: 'uint256' }], + }, +] -export function buildCreateSavedViewMessage(initiaAddress: string, viewKey: string) { +export function buildCreateSavedViewMessage( + initiaAddress: string, + viewKey: string, +) { const input = encodeFunctionData({ abi: REGISTRY_ABI, functionName: 'createSavedView', @@ -347,15 +359,17 @@ export function buildCreateSavedViewMessage(initiaAddress: string, viewKey: stri return { chainId: CHAIN_ID, - messages: [{ - typeUrl: '/minievm.evm.v1.MsgCall', - value: { - sender: initiaAddress.toLowerCase(), - contractAddr: CONTRACT_ADDRESS, - input, - value: '0', + messages: [ + { + typeUrl: '/minievm.evm.v1.MsgCall', + value: { + sender: initiaAddress.toLowerCase(), + contractAddr: CONTRACT_ADDRESS, + input, + value: '0', + }, }, - }], + ], } } ``` @@ -374,12 +388,16 @@ export function DuneTable({ rows }: { rows: any[] }) {
- {columns.map(col => )} + + {columns.map((col) => ( + + ))} + {rows.map((row, i) => ( - {columns.map(col => ( + {columns.map((col) => ( ))} @@ -398,18 +416,25 @@ Use `useInterwovenKit` to connect wallets and store preferences onchain. ```tsx frontend/src/App.tsx import { useEffect, useState } from 'react' import { useInterwovenKit } from '@initia/interwovenkit-react' -import { DUNE_QUERY_OPTIONS, fetchLatestDuneResult, getQueryConfig } from './lib/dune' +import { + DUNE_QUERY_OPTIONS, + fetchLatestDuneResult, + getQueryConfig, +} from './lib/dune' import { buildCreateSavedViewMessage } from './lib/contract' import { DuneTable } from './components/DuneTable' export function Analytics() { const { initiaAddress, requestTxBlock } = useInterwovenKit() - const [viewKey, setViewKey] = useState(DUNE_QUERY_OPTIONS[0]?.key ?? 'overview') + const [viewKey, setViewKey] = useState( + DUNE_QUERY_OPTIONS[0]?.key ?? 'overview', + ) const [data, setData] = useState([]) useEffect(() => { - void fetchLatestDuneResult(getQueryConfig(viewKey).queryId) - .then((res) => setData(res.result.rows)) + void fetchLatestDuneResult(getQueryConfig(viewKey).queryId).then((res) => + setData(res.result.rows), + ) }, [viewKey]) const handleSaveView = async (key: string) => { @@ -420,8 +445,10 @@ export function Analytics() { return (
@@ -432,15 +459,16 @@ export function Analytics() { ``` In a complete implementation, the frontend also loads saved views from the -contract, maps each saved `viewKey` back through `getQueryConfig`, and shows -the matching Dune result for the selected preference. The snippet above keeps -the core flow visible without repeating the full UI. +contract, maps each saved `viewKey` back through `getQueryConfig`, and shows the +matching Dune result for the selected preference. The snippet above keeps the +core flow visible without repeating the full UI. ## SQL Recipes for Initia Use these templates in the Dune Query Editor to build your analytics views. ### 1. Daily Transaction Volume + ```sql SELECT date_trunc('day', block_time) as day, @@ -452,6 +480,7 @@ GROUP BY 1 ORDER BY 1 DESC ``` ### 2. Popular Message Types + ```sql SELECT type_url, @@ -470,7 +499,8 @@ LIMIT 10 - **Access Control:** Always use the `DUNE_ALLOWED_QUERY_IDS` allowlist on your backend to prevent unauthorized proxying. - **Data Freshness:** Dune's results endpoint returns the latest saved - execution. For real-time state, use the [Initia Indexer](/api-reference/rollup-indexer/introduction). + execution. For real-time state, use the + [Initia Indexer](/api-reference/rollup-indexer/introduction). ## Resources From 761618a7382b0fe0c94e614b38fc4333982dbaa2 Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Thu, 7 May 2026 11:46:05 +0700 Subject: [PATCH 5/8] docs: restructure Dune integration guide and add saved-views read path --- .../guides/dune-integration.mdx | 233 ++++++++++++------ 1 file changed, 158 insertions(+), 75 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index 4dac2b9d..b7396c2f 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -41,16 +41,16 @@ implementation where: -## Dune App Capabilities +### Choosing the Right Tool -If you want to work directly in Dune before wiring anything into an Initia app, -the web app gives you the core building blocks you need: query editing, -dashboards, search/discovery, and scheduled refreshes. +Use the Initia Indexer for live app state and Dune for historical analysis, +reporting, and dashboards. - - Use Dune directly to prototype queries and dashboards. Use the API when you - want to embed saved results into an Initia app or backend flow. - +| Feature | [Initia Indexer](/api-reference/rollup-indexer/introduction) | Dune Analytics | +| --------------- | ------------------------------------------------------------ | ----------------------------------- | +| **Data Scope** | Real-time app state | Historical trends and aggregates | +| **Query Style** | Specific accounts and events | Complex SQL across millions of rows | +| **Primary Use** | Core app logic | Dashboards and public reports | ## Prerequisites @@ -63,6 +63,36 @@ dashboards, search/discovery, and scheduled refreshes. - **Registry Contract Address:** The deployed address for your saved-view contract. +## Step 0: Build Your Dune Queries + +Before wiring anything together, create the SQL queries your app will display. +Run these example templates in the Dune Query Editor and save each one to get a +numeric query ID. You will plug those IDs into the backend allowlist and the +frontend `.env` in the next steps. + +### 1. Daily Transaction Volume + +```sql +SELECT + date_trunc('day', block_time) as day, + count(*) as tx_count, + count(distinct "from") as unique_users +FROM initia.transactions +WHERE block_time > now() - interval '30 days' +GROUP BY 1 ORDER BY 1 DESC +``` + +### 2. Popular Message Types + +```sql +SELECT + type_url, + count(*) as count +FROM initia.tx_messages +GROUP BY 1 ORDER BY 2 DESC +LIMIT 10 +``` + ## Step 1: Backend Implementation Your backend acts as a secure proxy, ensuring your `DUNE_API_KEY` is never @@ -195,14 +225,8 @@ app.listen(port, () => { ## Step 2: Onchain Registry This example uses a MiniEVM Solidity contract to store user view preferences. -The same registry pattern can be implemented on MiniMove or MiniWasm chains with -the matching Move or CosmWasm contract/module instead. - - - The registry example is MiniEVM-specific, but the saved-view pattern itself is - VM-agnostic. Use the equivalent contract/module syntax for MiniMove or - MiniWasm if your rollup uses a different VM. - +The saved-view pattern is VM-agnostic, so you can implement the same registry +on MiniMove or MiniWasm by using the equivalent Move or CosmWasm syntax. ```solidity // SPDX-License-Identifier: MIT @@ -245,6 +269,16 @@ contract DuneRadarRegistry { } ``` +Deploy the contract to your rollup with Foundry, then set +`VITE_REGISTRY_ADDRESS` in your frontend `.env` to the address it prints. + +```bash +forge create src/DuneRadarRegistry.sol:DuneRadarRegistry \ + --rpc-url $JSON_RPC_URL \ + --private-key $DEPLOYER_KEY \ + --broadcast +``` + ## Step 3: Frontend Implementation The frontend uses **InterwovenKit** to manage onchain preferences and the proxy @@ -317,6 +351,7 @@ VITE_DUNE_QUERY_ID_BRIDGES=2345678 VITE_DUNE_QUERY_ID_WALLETS=3456789 VITE_APPCHAIN_ID=your-appchain-id VITE_REGISTRY_ADDRESS=0xYourDeployedRegistryContractAddress +VITE_JSON_RPC_URL=http://localhost:8545 ``` @@ -327,15 +362,15 @@ VITE_REGISTRY_ADDRESS=0xYourDeployedRegistryContractAddress ### 2. Contract Helper -This helper encodes the `MsgCall` for your registry contract using `viem` and -keeps the frontend example focused on the transaction payload rather than the -submission plumbing. +This helper encodes the `MsgCall` write for your registry contract and reads +saved views back through a JSON-RPC `eth_call`. Both paths reuse the same ABI. ```ts frontend/src/lib/contract.ts -import { encodeFunctionData } from 'viem' +import { decodeFunctionResult, encodeFunctionData } from 'viem' const CONTRACT_ADDRESS = import.meta.env.VITE_REGISTRY_ADDRESS const CHAIN_ID = import.meta.env.VITE_APPCHAIN_ID +const JSON_RPC_URL = import.meta.env.VITE_JSON_RPC_URL export const REGISTRY_ABI = [ { @@ -345,6 +380,24 @@ export const REGISTRY_ABI = [ inputs: [{ name: 'viewKey', type: 'string' }], outputs: [{ name: 'savedViewId', type: 'uint256' }], }, + { + name: 'getSavedViews', + type: 'function', + stateMutability: 'view', + inputs: [{ name: 'owner', type: 'address' }], + outputs: [ + { + type: 'tuple[]', + components: [ + { name: 'id', type: 'uint256' }, + { name: 'owner', type: 'address' }, + { name: 'viewKey', type: 'string' }, + { name: 'createdAt', type: 'uint64' }, + { name: 'archived', type: 'bool' }, + ], + }, + ], + }, ] export function buildCreateSavedViewMessage( @@ -367,11 +420,49 @@ export function buildCreateSavedViewMessage( contractAddr: CONTRACT_ADDRESS, input, value: '0', + accessList: [], + authList: [], }, }, ], } } + +export async function fetchSavedViews(hexAddress: string) { + if (!hexAddress || !CONTRACT_ADDRESS) return [] + + const data = encodeFunctionData({ + abi: REGISTRY_ABI, + functionName: 'getSavedViews', + args: [hexAddress], + }) + + const response = await fetch(JSON_RPC_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'eth_call', + params: [{ to: CONTRACT_ADDRESS, data }, 'latest'], + }), + }) + + const { result, error } = await response.json() + if (error) throw new Error(error.message) + + const decoded = decodeFunctionResult({ + abi: REGISTRY_ABI, + functionName: 'getSavedViews', + data: result, + }) + + return decoded.map((view) => ({ + ...view, + id: Number(view.id), + createdAt: Number(view.createdAt), + })) +} ``` ### 3. Rendering Results @@ -411,35 +502,56 @@ export function DuneTable({ rows }: { rows: any[] }) { ### 4. Integration Example -Use `useInterwovenKit` to connect wallets and store preferences onchain. +Use `useInterwovenKit` for the write path and `useHexAddress` for the read. +The component loads the user's saved views on mount, lets them open any saved +view to see the matching Dune result, and refreshes the list after a new +preference is saved. ```tsx frontend/src/App.tsx -import { useEffect, useState } from 'react' -import { useInterwovenKit } from '@initia/interwovenkit-react' +import { useCallback, useEffect, useState } from 'react' +import { + useHexAddress, + useInterwovenKit, +} from '@initia/interwovenkit-react' import { DUNE_QUERY_OPTIONS, fetchLatestDuneResult, getQueryConfig, } from './lib/dune' -import { buildCreateSavedViewMessage } from './lib/contract' +import { + buildCreateSavedViewMessage, + fetchSavedViews, +} from './lib/contract' import { DuneTable } from './components/DuneTable' export function Analytics() { const { initiaAddress, requestTxBlock } = useInterwovenKit() + const hexAddress = useHexAddress() const [viewKey, setViewKey] = useState( DUNE_QUERY_OPTIONS[0]?.key ?? 'overview', ) - const [data, setData] = useState([]) + const [savedViews, setSavedViews] = useState([]) + const [rows, setRows] = useState([]) + + const loadSavedViews = useCallback(async () => { + if (!hexAddress) return + setSavedViews(await fetchSavedViews(hexAddress)) + }, [hexAddress]) useEffect(() => { - void fetchLatestDuneResult(getQueryConfig(viewKey).queryId).then((res) => - setData(res.result.rows), - ) + void loadSavedViews() + }, [loadSavedViews]) + + useEffect(() => { + const queryId = getQueryConfig(viewKey).queryId + if (!queryId) return + void fetchLatestDuneResult(queryId).then((res) => setRows(res.result.rows)) }, [viewKey]) - const handleSaveView = async (key: string) => { + const handleSaveView = async () => { if (!initiaAddress) return - await requestTxBlock(buildCreateSavedViewMessage(initiaAddress, key)) + await requestTxBlock(buildCreateSavedViewMessage(initiaAddress, viewKey)) + await loadSavedViews() } return ( @@ -451,45 +563,26 @@ export function Analytics() { ))} - - + + +
    + {savedViews + .filter((view) => !view.archived) + .map((view) => ( +
  • + +
  • + ))} +
+ +
) } ``` -In a complete implementation, the frontend also loads saved views from the -contract, maps each saved `viewKey` back through `getQueryConfig`, and shows the -matching Dune result for the selected preference. The snippet above keeps the -core flow visible without repeating the full UI. - -## SQL Recipes for Initia - -Use these templates in the Dune Query Editor to build your analytics views. - -### 1. Daily Transaction Volume - -```sql -SELECT - date_trunc('day', block_time) as day, - count(*) as tx_count, - count(distinct "from") as unique_users -FROM initia.transactions -WHERE block_time > now() - interval '30 days' -GROUP BY 1 ORDER BY 1 DESC -``` - -### 2. Popular Message Types - -```sql -SELECT - type_url, - count(*) as count -FROM initia.tx_messages -GROUP BY 1 ORDER BY 2 DESC -LIMIT 10 -``` - ## Troubleshooting and Security Tips - **API Key Security:** **Never** expose `DUNE_API_KEY` in your frontend. It @@ -502,20 +595,10 @@ LIMIT 10 execution. For real-time state, use the [Initia Indexer](/api-reference/rollup-indexer/introduction). -## Resources - -### Common Dune Example Tables +## Common Dune Tables for Initia | Table | Recommended Use | | ------------------------- | -------------------------------------- | | `initia.transactions` | Network activity and gas trends. | | `initia.bridge_transfers` | Asset flow and bridge route analytics. | | `initia.tx_messages` | Detailed message-type activity. | - -### Choosing the Right Tool - -| Feature | [Initia Indexer](/api-reference/rollup-indexer/introduction) | Dune Analytics | -| --------------- | ------------------------------------------------------------ | ------------------------------------ | -| **Data Scope** | Real-time app state. | Historical trends & aggregates. | -| **Query Style** | Specific accounts/events. | Complex SQL across millions of rows. | -| **Primary Use** | Core app logic. | Dashboards & public reports. | From 123a84a95a65a5049eb152fb3daa36fd6c9bb279 Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Thu, 7 May 2026 13:07:55 +0700 Subject: [PATCH 6/8] style: Prettier formatting --- .../guides/dune-integration.mdx | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index b7396c2f..e8889412 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -225,8 +225,8 @@ app.listen(port, () => { ## Step 2: Onchain Registry This example uses a MiniEVM Solidity contract to store user view preferences. -The saved-view pattern is VM-agnostic, so you can implement the same registry -on MiniMove or MiniWasm by using the equivalent Move or CosmWasm syntax. +The saved-view pattern is VM-agnostic, so you can implement the same registry on +MiniMove or MiniWasm by using the equivalent Move or CosmWasm syntax. ```solidity // SPDX-License-Identifier: MIT @@ -502,26 +502,20 @@ export function DuneTable({ rows }: { rows: any[] }) { ### 4. Integration Example -Use `useInterwovenKit` for the write path and `useHexAddress` for the read. -The component loads the user's saved views on mount, lets them open any saved -view to see the matching Dune result, and refreshes the list after a new -preference is saved. +Use `useInterwovenKit` for the write path and `useHexAddress` for the read. The +component loads the user's saved views on mount, lets them open any saved view +to see the matching Dune result, and refreshes the list after a new preference +is saved. ```tsx frontend/src/App.tsx import { useCallback, useEffect, useState } from 'react' -import { - useHexAddress, - useInterwovenKit, -} from '@initia/interwovenkit-react' +import { useHexAddress, useInterwovenKit } from '@initia/interwovenkit-react' import { DUNE_QUERY_OPTIONS, fetchLatestDuneResult, getQueryConfig, } from './lib/dune' -import { - buildCreateSavedViewMessage, - fetchSavedViews, -} from './lib/contract' +import { buildCreateSavedViewMessage, fetchSavedViews } from './lib/contract' import { DuneTable } from './components/DuneTable' export function Analytics() { From 09d1093150a348593f7fcc920b35874bc07853c2 Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Thu, 7 May 2026 14:31:29 +0700 Subject: [PATCH 7/8] docs: fix Initia Dune examples and add query discovery tip --- .../guides/dune-integration.mdx | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index e8889412..507f1ee6 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -70,26 +70,37 @@ Run these example templates in the Dune Query Editor and save each one to get a numeric query ID. You will plug those IDs into the backend allowlist and the frontend `.env` in the next steps. + + You can also browse public examples on the [query page](https://dune.com/queries) + when signed in. For table-specific ideas, search for `Initia` in Dune's data + catalog. + + ### 1. Daily Transaction Volume ```sql SELECT - date_trunc('day', block_time) as day, - count(*) as tx_count, - count(distinct "from") as unique_users -FROM initia.transactions -WHERE block_time > now() - interval '30 days' -GROUP BY 1 ORDER BY 1 DESC + date_trunc('day', b.block_timestamp) AS day, + count(*) AS tx_count, + count(distinct t.fee_payer) AS unique_fee_payers +FROM initia.transactions t +JOIN initia.blocks b + ON t.chain_id = b.chain_id + AND t.block_height = b.block_height +WHERE b.block_date >= CURRENT_DATE - INTERVAL '30' DAY +GROUP BY 1 +ORDER BY 1 DESC ``` ### 2. Popular Message Types ```sql SELECT - type_url, - count(*) as count + message_type, + count(*) AS count FROM initia.tx_messages -GROUP BY 1 ORDER BY 2 DESC +GROUP BY 1 +ORDER BY 2 DESC LIMIT 10 ``` From a444d441855727cf4e6f5ec2ee3501b8dcf62328 Mon Sep 17 00:00:00 2001 From: Manuel Alessandro Collazo Date: Thu, 7 May 2026 14:32:34 +0700 Subject: [PATCH 8/8] style: Prettier formatting --- developers/developer-guides/guides/dune-integration.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/developers/developer-guides/guides/dune-integration.mdx b/developers/developer-guides/guides/dune-integration.mdx index 507f1ee6..683456d5 100644 --- a/developers/developer-guides/guides/dune-integration.mdx +++ b/developers/developer-guides/guides/dune-integration.mdx @@ -71,9 +71,9 @@ numeric query ID. You will plug those IDs into the backend allowlist and the frontend `.env` in the next steps. - You can also browse public examples on the [query page](https://dune.com/queries) - when signed in. For table-specific ideas, search for `Initia` in Dune's data - catalog. + You can also browse public examples on the [query + page](https://dune.com/queries) when signed in. For table-specific ideas, + search for `Initia` in Dune's data catalog. ### 1. Daily Transaction Volume
{col}
{col}
{String(row[col] ?? '—')}