From 0183ed76760f4c5a8c22a148b34eb3bd62e8c06a Mon Sep 17 00:00:00 2001 From: kamaldeen Aliyu Date: Sun, 22 Feb 2026 13:13:23 +0100 Subject: [PATCH] Created the Total Value Locked --- .../src/graphql/resolvers/vaultResolver.ts | 9 ++ backend/src/index.js | 23 ++++ backend/src/models/index.js | 4 +- backend/src/models/tvl.js | 43 ++++++ backend/src/services/indexingService.js | 9 ++ backend/src/services/tvlService.js | 130 ++++++++++++++++++ 6 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 backend/src/models/tvl.js create mode 100644 backend/src/services/tvlService.js diff --git a/backend/src/graphql/resolvers/vaultResolver.ts b/backend/src/graphql/resolvers/vaultResolver.ts index f03f97b..5ea2e85 100644 --- a/backend/src/graphql/resolvers/vaultResolver.ts +++ b/backend/src/graphql/resolvers/vaultResolver.ts @@ -1,5 +1,6 @@ const models = require('../../models'); const cacheService = require('../../services/cacheService'); +const tvlService = require('../../services/tvlService'); export const vaultResolver = { Query: { @@ -131,6 +132,14 @@ export const vaultResolver = { console.log(`Invalidated cache for user vaults: ${input.ownerAddress}`); } + // Update TVL for new vault + try { + await tvlService.handleVaultCreated(vault.toJSON()); + } catch (tvlError) { + console.error('Error updating TVL for new vault:', tvlError); + // Don't throw - TVL update failure shouldn't fail vault creation + } + return vault; } catch (error) { console.error('Error creating vault:', error); diff --git a/backend/src/index.js b/backend/src/index.js index 15bbb1c..8c060af 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -32,6 +32,7 @@ const adminService = require('./services/adminService'); const vestingService = require('./services/vestingService'); const discordBotService = require('./services/discordBotService'); const cacheService = require('./services/cacheService'); +const tvlService = require('./services/tvlService'); // Routes app.get('/', (req, res) => { @@ -220,6 +221,28 @@ app.get('/api/admin/pending-transfers', async (req, res) => { } }); +// Stats Routes +app.get('/api/stats/tvl', async (req, res) => { + try { + const tvlStats = await tvlService.getTVLStats(); + res.json({ + success: true, + data: { + total_value_locked: tvlStats.total_value_locked, + active_vaults_count: tvlStats.active_vaults_count, + formatted_tvl: tvlService.formatTVL(tvlStats.total_value_locked), + last_updated_at: tvlStats.last_updated_at + } + }); + } catch (error) { + console.error('Error fetching TVL stats:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + // Start server const startServer = async () => { try { diff --git a/backend/src/models/index.js b/backend/src/models/index.js index 49ad4a8..a7c9bad 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -2,13 +2,13 @@ const { sequelize } = require('../database/connection'); const ClaimsHistory = require('./claimsHistory'); const Vault = require('./vault'); const SubSchedule = require('./subSchedule'); - +const TVL = require('./tvl'); const models = { ClaimsHistory, Vault, SubSchedule, - + TVL, sequelize, }; diff --git a/backend/src/models/tvl.js b/backend/src/models/tvl.js new file mode 100644 index 0000000..c25c218 --- /dev/null +++ b/backend/src/models/tvl.js @@ -0,0 +1,43 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../database/connection'); + +const TVL = sequelize.define('TVL', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + total_value_locked: { + type: DataTypes.DECIMAL(36, 18), + allowNull: false, + defaultValue: 0, + comment: 'Total value locked across all active vaults', + }, + active_vaults_count: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0, + comment: 'Number of active vaults', + }, + last_updated_at: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW, + comment: 'Last time TVL was updated', + }, + created_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, + updated_at: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW, + }, +}, { + tableName: 'tvl', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', +}); + +module.exports = TVL; diff --git a/backend/src/services/indexingService.js b/backend/src/services/indexingService.js index cee6074..04d5f92 100644 --- a/backend/src/services/indexingService.js +++ b/backend/src/services/indexingService.js @@ -1,6 +1,7 @@ const { ClaimsHistory, Vault, SubSchedule } = require('../models'); const priceService = require('./priceService'); const slackWebhookService = require('./slackWebhookService'); +const tvlService = require('./tvlService'); class IndexingService { async processClaim(claimData) { @@ -41,6 +42,14 @@ class IndexingService { // Don't throw - alert failure shouldn't fail the claim processing } + // Update TVL for claim event + try { + await tvlService.handleClaim(claim.toJSON()); + } catch (tvlError) { + console.error('Error updating TVL for claim:', tvlError); + // Don't throw - TVL update failure shouldn't fail claim processing + } + return claim; } catch (error) { console.error('Error processing claim:', error); diff --git a/backend/src/services/tvlService.js b/backend/src/services/tvlService.js new file mode 100644 index 0000000..790ffea --- /dev/null +++ b/backend/src/services/tvlService.js @@ -0,0 +1,130 @@ +const { Vault, TVL } = require('../models'); + +class TVLService { + /** + * Calculate total TVL from all active vaults + * @returns {Promise<{totalValueLocked: number, activeVaultsCount: number}>} + */ + async calculateTVL() { + try { + const vaults = await Vault.findAll({ + where: { is_active: true } + }); + + let totalValueLocked = 0; + for (const vault of vaults) { + totalValueLocked += parseFloat(vault.total_amount || 0); + } + + return { + totalValueLocked, + activeVaultsCount: vaults.length + }; + } catch (error) { + console.error('Error calculating TVL:', error); + throw error; + } + } + + /** + * Update TVL record in database + * @returns {Promise} Updated TVL record + */ + async updateTVL() { + try { + const { totalValueLocked, activeVaultsCount } = await this.calculateTVL(); + + // Get or create TVL record (there should only be one) + let tvlRecord = await TVL.findOne(); + + if (tvlRecord) { + await tvlRecord.update({ + total_value_locked: totalValueLocked, + active_vaults_count: activeVaultsCount, + last_updated_at: new Date() + }); + } else { + tvlRecord = await TVL.create({ + total_value_locked: totalValueLocked, + active_vaults_count: activeVaultsCount, + last_updated_at: new Date() + }); + } + + console.log(`TVL updated: ${totalValueLocked} across ${activeVaultsCount} vaults`); + return tvlRecord; + } catch (error) { + console.error('Error updating TVL:', error); + throw error; + } + } + + /** + * Get current TVL stats + * @returns {Promise} TVL stats + */ + async getTVLStats() { + try { + let tvlRecord = await TVL.findOne(); + + // If no record exists, calculate and create one + if (!tvlRecord) { + tvlRecord = await this.updateTVL(); + } + + return { + total_value_locked: parseFloat(tvlRecord.total_value_locked), + active_vaults_count: tvlRecord.active_vaults_count, + last_updated_at: tvlRecord.last_updated_at, + created_at: tvlRecord.created_at + }; + } catch (error) { + console.error('Error getting TVL stats:', error); + throw error; + } + } + + /** + * Handle vault created event - increment TVL + * @param {Object} vaultData - New vault data + * @returns {Promise} + */ + async handleVaultCreated(vaultData) { + try { + console.log(`Handling VaultCreated event for vault: ${vaultData.address}`); + await this.updateTVL(); + } catch (error) { + console.error('Error handling vault created event:', error); + } + } + + /** + * Handle claim event - decrement TVL by claimed amount + * @param {Object} claimData - Claim data + * @returns {Promise} + */ + async handleClaim(claimData) { + try { + console.log(`Handling Claim event for transaction: ${claimData.transaction_hash}`); + await this.updateTVL(); + } catch (error) { + console.error('Error handling claim event:', error); + } + } + + /** + * Format TVL value to human-readable string + * @param {number} tvl - TVL value + * @returns {string} Formatted TVL string (e.g., "$5M", "$500K") + */ + formatTVL(tvl) { + if (tvl >= 1000000) { + return `$${(tvl / 1000000).toFixed(2)}M`; + } else if (tvl >= 1000) { + return `$${(tvl / 1000).toFixed(2)}K`; + } + return `$${tvl.toFixed(2)}`; + } +} + +module.exports = new TVLService();