diff --git a/backend/internal/data/shutter_explorer.sql.go b/backend/internal/data/shutter_explorer.sql.go
index 34984c8..ef409f8 100644
--- a/backend/internal/data/shutter_explorer.sql.go
+++ b/backend/internal/data/shutter_explorer.sql.go
@@ -84,6 +84,32 @@ func (q *Queries) QueryExecutedTransactionStats(ctx context.Context) ([]QueryExe
return items, nil
}
+const queryExecutedTransactionStatsRecent = `-- name: QueryExecutedTransactionStatsRecent :many
+SELECT COUNT(id), tx_status FROM decrypted_tx
+WHERE created_at >= NOW() - ($1::int * INTERVAL '1 day')
+GROUP BY tx_status
+`
+
+func (q *Queries) QueryExecutedTransactionStatsRecent(ctx context.Context, days int32) ([]QueryExecutedTransactionStatsRow, error) {
+ rows, err := q.db.Query(ctx, queryExecutedTransactionStatsRecent, days)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []QueryExecutedTransactionStatsRow
+ for rows.Next() {
+ var i QueryExecutedTransactionStatsRow
+ if err := rows.Scan(&i.Count, &i.TxStatus); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const queryFromTransactionDetails = `-- name: QueryFromTransactionDetails :many
SELECT tx_hash as user_tx_hash, encrypted_tx_hash
FROM transaction_details
diff --git a/backend/internal/data/sql/queries/shutter_explorer.sql b/backend/internal/data/sql/queries/shutter_explorer.sql
index c7b5acd..3ee4568 100644
--- a/backend/internal/data/sql/queries/shutter_explorer.sql
+++ b/backend/internal/data/sql/queries/shutter_explorer.sql
@@ -181,6 +181,11 @@ LIMIT $1;
SELECT COUNT(id), tx_status FROM decrypted_tx
GROUP BY tx_status;
+-- name: QueryExecutedTransactionStatsRecent :many
+SELECT COUNT(id), tx_status FROM decrypted_tx
+WHERE created_at >= NOW() - ($1::int * INTERVAL '1 day')
+GROUP BY tx_status;
+
-- name: QueryHistoricalInclusionTimes :many
WITH daily_inclusion_times AS (
SELECT
diff --git a/backend/internal/service/inclusion_time.go b/backend/internal/service/inclusion_time.go
index bea4237..04414f8 100644
--- a/backend/internal/service/inclusion_time.go
+++ b/backend/internal/service/inclusion_time.go
@@ -2,6 +2,7 @@ package service
import (
"net/http"
+ "strconv"
"github.com/gin-gonic/gin"
"github.com/shutter-network/shutter-explorer/backend/internal/usecase"
@@ -39,6 +40,23 @@ func (svc *InclusionTimeService) QueryExecutedTransactionStats(ctx *gin.Context)
})
}
+func (svc *InclusionTimeService) QueryExecutedTransactionStatsRecent(ctx *gin.Context) {
+ daysStr := ctx.DefaultQuery("days", "30")
+ days, err := strconv.Atoi(daysStr)
+ if err != nil || days <= 0 {
+ ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid days parameter"})
+ return
+ }
+ stats, httpErr := svc.InclusionTimeUsecase.QueryExecutedTransactionStatsRecent(ctx, days)
+ if httpErr != nil {
+ ctx.Error(httpErr)
+ return
+ }
+ ctx.JSON(http.StatusOK, gin.H{
+ "message": stats,
+ })
+}
+
func (svc *InclusionTimeService) QueryHistoricalInclusionTimes(ctx *gin.Context) {
historicalInclusionTimes, err := svc.InclusionTimeUsecase.QueryHistoricalInclusionTimes(ctx)
if err != nil {
diff --git a/backend/internal/usecase/inclusion_time.go b/backend/internal/usecase/inclusion_time.go
index 6803389..7c8cc8a 100644
--- a/backend/internal/usecase/inclusion_time.go
+++ b/backend/internal/usecase/inclusion_time.go
@@ -109,6 +109,40 @@ func (uc *InclusionTimeUsecase) QueryExecutedTransactionStats(ctx context.Contex
return resp, nil
}
+func (uc *InclusionTimeUsecase) QueryExecutedTransactionStatsRecent(ctx context.Context, days int) (*QueryExectuedTransactionStatsResp, *error.Http) {
+ stats, err := uc.observerDBQuery.QueryExecutedTransactionStatsRecent(ctx, int32(days))
+ if err != nil {
+ log.Err(err).Msg("err encountered while querying DB")
+ err := error.NewHttpError(
+ "error encountered while querying for data",
+ "",
+ http.StatusInternalServerError,
+ )
+ return nil, &err
+ }
+
+ resp := &QueryExectuedTransactionStatsResp{}
+ for i := 0; i < len(stats); i++ {
+ resp.Total += stats[i].Count
+ switch stats[i].TxStatus {
+ case data.TxStatusValInvalid:
+ resp.Invalid = stats[i].Count
+ case data.TxStatusValNotdecrypted:
+ resp.NotDecrypted = stats[i].Count
+ case data.TxStatusValNotincluded:
+ resp.NotIncluded = stats[i].Count
+ case data.TxStatusValPending:
+ resp.Pending = stats[i].Count
+ case data.TxStatusValShieldedinclusion:
+ resp.Shielded = stats[i].Count
+ case data.TxStatusValUnshieldedinclusion:
+ resp.Unshielded = stats[i].Count
+ }
+ }
+
+ return resp, nil
+}
+
func (uc *InclusionTimeUsecase) QueryHistoricalInclusionTimes(ctx context.Context) ([]data.QueryHistoricalInclusionTimesRow, *error.Http) {
historicalInclusionTimes, err := uc.observerDBQuery.QueryHistoricalInclusionTimes(ctx)
if err != nil {
diff --git a/backend/router/router.go b/backend/router/router.go
index 22a356e..1f0a6e1 100644
--- a/backend/router/router.go
+++ b/backend/router/router.go
@@ -52,6 +52,7 @@ func NewRouter(ctx context.Context, usecases *usecase.Usecases) *gin.Engine {
inclusionTime := api.Group("inclusion_time")
inclusionTime.GET("/estimated_inclusion_time", inclusionTimeService.QueryEstimatedInclusionTime)
inclusionTime.GET("/executed_transactions", inclusionTimeService.QueryExecutedTransactionStats)
+ inclusionTime.GET("/executed_transactions_recent", inclusionTimeService.QueryExecutedTransactionStatsRecent)
inclusionTime.GET("/historical_inclusion_time", inclusionTimeService.QueryHistoricalInclusionTimes)
}
return router
diff --git a/frontend/cypress/components/TransactionGauge.cy.tsx b/frontend/cypress/components/TransactionGauge.cy.tsx
index d074025..c7412a9 100644
--- a/frontend/cypress/components/TransactionGauge.cy.tsx
+++ b/frontend/cypress/components/TransactionGauge.cy.tsx
@@ -7,16 +7,29 @@ import { ThemeProvider as MUIThemeProvider } from '@mui/material/styles';
import { ThemeProvider as StyledThemeProvider } from 'styled-components';
import { customTheme, muiTheme } from '../../src/theme';
+const mountGauge = () => {
+ const mockSocket = {
+ onopen: cy.stub(),
+ onmessage: cy.stub(),
+ onclose: cy.stub(),
+ onerror: cy.stub(),
+ };
+ return mount(
+
+
+
+
+
+
+
+
+
+ );
+};
+
describe('', () => {
- it('renders the transaction gauge with test values', () => {
- const mockSocket = {
- onopen: cy.stub(),
- onmessage: cy.stub(),
- onclose: cy.stub(),
- onerror: cy.stub(),
- };
-
- cy.intercept('GET', '/api/inclusion_time/executed_transactions', {
+ it('renders the gauge with default 30-day window', () => {
+ cy.intercept('GET', '/api/inclusion_time/executed_transactions_recent?days=30', {
statusCode: 200,
body: {
message: {
@@ -25,32 +38,42 @@ describe('', () => {
Total: 30
}
},
- }).as('getExecutedTransactions');
-
- mount(
-
-
-
-
-
-
-
-
-
- );
-
- cy.wait('@getExecutedTransactions');
+ }).as('getRecentTransactions');
+
+ mountGauge();
+
+ cy.wait('@getRecentTransactions');
cy.get('div[role="meter"]').invoke('css', 'height', '300px');
cy.get('div[role="meter"]').should('exist').and('be.visible');
+ cy.contains('Last 30 days').should('be.visible');
cy.contains('Shielded').should('be.visible');
cy.contains('25').should('be.visible');
-
cy.contains('Total').should('be.visible');
cy.contains('30').should('be.visible');
-
cy.contains('Unshielded').should('be.visible');
cy.contains('5').should('be.visible');
});
+
+ it('switches to 7-day window when 7d button is clicked', () => {
+ cy.intercept('GET', '/api/inclusion_time/executed_transactions_recent?days=30', {
+ statusCode: 200,
+ body: { message: { Shielded: 25, Unshielded: 5 } },
+ });
+
+ cy.intercept('GET', '/api/inclusion_time/executed_transactions_recent?days=7', {
+ statusCode: 200,
+ body: { message: { Shielded: 10, Unshielded: 2 } },
+ }).as('get7DayTransactions');
+
+ mountGauge();
+
+ cy.contains('7d').click();
+ cy.wait('@get7DayTransactions');
+
+ cy.contains('Last 7 days').should('be.visible');
+ cy.contains('10').should('be.visible');
+ cy.contains('2').should('be.visible');
+ });
});
diff --git a/frontend/src/modules/TransactionGauge.tsx b/frontend/src/modules/TransactionGauge.tsx
index 774610f..9ae1e07 100644
--- a/frontend/src/modules/TransactionGauge.tsx
+++ b/frontend/src/modules/TransactionGauge.tsx
@@ -1,64 +1,54 @@
-import { Alert, Box } from '@mui/material';
+import { Alert, Box, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material';
import OverviewCard from '../components/OverviewCard';
import BasicGauges from '../components/Gauge';
-import { useEffect, useState } from 'react';
-import { useWebSocket } from '../context/WebSocketContext';
+import { useState } from 'react';
import useFetch from '../hooks/useFetch';
-const TransactionGauge = () => {
- const { data: transactionStatsData, loading: loadingTransactionStats, error: errorTransactionStats } = useFetch('/api/inclusion_time/executed_transactions');
- const [successfulTransactions, setSuccessfulTransactions] = useState(transactionStatsData?.message?.Shielded || 0);
- const [failedTransactions, setFailedTransactions] = useState(transactionStatsData?.message?.Unshielded || 0);
- const [, setWebSocketError] = useState(null);
-
- const { socket } = useWebSocket()!;
-
- useEffect(() => {
- if (socket) {
- socket.onmessage = (event: MessageEvent) => {
- const websocketEvent = JSON.parse(event.data);
- if (websocketEvent.error) {
- setWebSocketError(`Error: ${websocketEvent.error.message} (Code: ${websocketEvent.error.code})`);
- } else if (websocketEvent.data) {
- setWebSocketError(null);
- if (websocketEvent.type === 'executed_transactions_updated') {
- if ('Shielded' in websocketEvent.data.message && 'Unshielded' in websocketEvent.data.message) {
- setSuccessfulTransactions(websocketEvent.data.message.Shielded);
- setFailedTransactions(websocketEvent.data.message.Unshielded);
- }
- }
- }
- };
+const WINDOW_OPTIONS = [7, 30, 90, 'all'] as const;
+type WindowOption = typeof WINDOW_OPTIONS[number];
- socket.onerror = () => {
- setWebSocketError('WebSocket error: A connection error occurred');
- };
- }
-
- return () => {
- if (socket) {
- socket.onmessage = null;
- socket.onerror = null;
- }
- };
- }, [socket]);
+const TransactionGauge = () => {
+ const [window, setWindow] = useState(30);
+ const url = window === 'all'
+ ? '/api/inclusion_time/executed_transactions'
+ : `/api/inclusion_time/executed_transactions_recent?days=${window}`;
+ const { data, loading, error } = useFetch(url);
- useEffect(() => {
- if (transactionStatsData?.message?.Shielded) setSuccessfulTransactions(transactionStatsData.message.Shielded);
- if (transactionStatsData?.message?.Unshielded) setFailedTransactions(transactionStatsData.message.Unshielded);
- }, [transactionStatsData]);
+ const shielded: number = data?.message?.Shielded || 0;
+ const unshielded: number = data?.message?.Unshielded || 0;
+ const total = shielded + unshielded;
- const totalTransactions = successfulTransactions + failedTransactions
return (
- {errorTransactionStats ? (
- Error fetching Transaction Stats: {errorTransactionStats.message}
+
+ { if (v !== null) setWindow(v as WindowOption); }}
+ size="small"
+ >
+ {WINDOW_OPTIONS.map((d) => (
+
+ {d === 'all' ? 'All' : `${d}d`}
+
+ ))}
+
+
+
+ {window === 'all' ? 'All time' : `Last ${window} days`}
+
+ {error ? (
+ Error fetching Transaction Stats: {error.message}
) : (
)}
@@ -66,4 +56,4 @@ const TransactionGauge = () => {
);
};
-export default TransactionGauge;
\ No newline at end of file
+export default TransactionGauge;