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 e1e79b7..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,48 +221,24 @@ app.get('/api/admin/pending-transfers', async (req, res) => { } }); -// Organization Routes -app.get('/api/org/:address', async (req, res) => { +// Stats Routes +app.get('/api/stats/tvl', async (req, res) => { try { - const { address } = req.params; - - // Validate address format (basic validation) - if (!address || address.length < 20) { - return res.status(400).json({ - success: false, - error: 'Valid admin address required' - }); - } - - const organization = await models.Organization.findOne({ - where: { admin_address: address } - }); - - if (!organization) { - return res.status(404).json({ - success: false, - error: 'Organization not found for this admin address' - }); - } - - res.json({ - success: true, + const tvlStats = await tvlService.getTVLStats(); + res.json({ + success: true, data: { - id: organization.id, - name: organization.name, - logo_url: organization.logo_url, - website_url: organization.website_url, - discord_url: organization.discord_url, - admin_address: organization.admin_address, - created_at: organization.created_at, - updated_at: organization.updated_at + 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 organization:', error); - res.status(500).json({ - success: false, - error: error.message + console.error('Error fetching TVL stats:', error); + res.status(500).json({ + success: false, + error: error.message }); } }); diff --git a/backend/src/models/index.js b/backend/src/models/index.js index ecf4ae4..e62a26a 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -2,15 +2,13 @@ const { sequelize } = require('../database/connection'); const ClaimsHistory = require('./claimsHistory'); const Vault = require('./vault'); const SubSchedule = require('./subSchedule'); -const Organization = require('./organization'); - +const TVL = require('./tvl'); const models = { ClaimsHistory, Vault, SubSchedule, - Organization, - + 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();