Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/src/graphql/resolvers/vaultResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const models = require('../../models');
const cacheService = require('../../services/cacheService');
const tvlService = require('../../services/tvlService');

export const vaultResolver = {
Query: {
Expand Down Expand Up @@ -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);
Expand Down
51 changes: 14 additions & 37 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
});
}
});
Expand Down
6 changes: 2 additions & 4 deletions backend/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down
43 changes: 43 additions & 0 deletions backend/src/models/tvl.js
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 9 additions & 0 deletions backend/src/services/indexingService.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
130 changes: 130 additions & 0 deletions backend/src/services/tvlService.js
Original file line number Diff line number Diff line change
@@ -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<TVL>} 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<Object>} 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<void>}
*/
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<void>}
*/
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();