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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
node_modules
7 changes: 7 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ REDIS_URL=redis://localhost:6379

# Slack Webhook Configuration
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL

# Email Configuration
EMAIL_HOST=smtp.mailtrap.io
EMAIL_PORT=2525
EMAIL_USER=your-email-user
EMAIL_PASS=your-email-password
EMAIL_FROM=no-reply@vestingvault.com
20 changes: 20 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,21 @@
"@graphql-tools/schema": "^10.0.2",
"axios": "^1.6.2",
"cors": "^2.8.5",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"graphql": "^16.8.1",
"graphql-subscriptions": "^2.0.0",
"graphql-ws": "^5.14.3",
"node-cron": "^4.2.1",
"nodemailer": "^8.0.1",
"pg": "^8.11.3",
"redis": "^4.6.12",
"sequelize": "^6.35.2",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
"ws": "^8.14.2",
"discord.js": "^14.14.1",
"redis": "^4.6.12"
"ws": "^8.14.2"
},
"devDependencies": {
"jest": "^29.7.0",
Expand Down
10 changes: 9 additions & 1 deletion backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const discordBotService = require('./services/discordBotService');
const cacheService = require('./services/cacheService');
const tvlService = require('./services/tvlService');
const vaultExportService = require('./services/vaultExportService');
const notificationService = require('./services/notificationService');

// Routes
app.get('/', (req, res) => {
Expand Down Expand Up @@ -308,7 +309,7 @@ const startServer = async () => {
await sequelize.authenticate();
console.log('Database connection established successfully.');

await sequelize.sync();
await sequelize.sync({ alter: true });
console.log('Database synchronized successfully.');

// Initialize Redis Cache
Expand Down Expand Up @@ -348,6 +349,13 @@ const startServer = async () => {
console.log('Continuing without Discord bot...');
}

// Initialize Notification Service (Cron Job)
try {
notificationService.start();
} catch (notificationError) {
console.error('Failed to start Notification Service:', notificationError);
}

// Start the HTTP server
httpServer.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
Expand Down
46 changes: 45 additions & 1 deletion backend/src/models/associations.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Vault, SubSchedule, Beneficiary, Organization } = require('../models');
const { Vault, SubSchedule, Beneficiary, Organization, Notification } = require('../models');

// Setup model associations
Vault.hasMany(SubSchedule, {
Expand All @@ -23,6 +23,27 @@ Beneficiary.belongsTo(Vault, {
as: 'vault',
});

Beneficiary.hasMany(Notification, {
foreignKey: 'beneficiary_id',
as: 'notifications',
onDelete: 'CASCADE',
});

Notification.belongsTo(Beneficiary, {
foreignKey: 'beneficiary_id',
as: 'beneficiary',
});

Notification.belongsTo(Vault, {
foreignKey: 'vault_id',
as: 'vault',
});

Notification.belongsTo(SubSchedule, {
foreignKey: 'sub_schedule_id',
as: 'subSchedule',
});

// Add associate methods to models
Vault.associate = function(models) {
Vault.hasMany(models.SubSchedule, {
Expand Down Expand Up @@ -60,11 +81,34 @@ Beneficiary.associate = function(models) {
foreignKey: 'vault_id',
as: 'vault',
});

Beneficiary.hasMany(models.Notification, {
foreignKey: 'beneficiary_id',
as: 'notifications',
});
};

Notification.associate = function(models) {
Notification.belongsTo(models.Beneficiary, {
foreignKey: 'beneficiary_id',
as: 'beneficiary',
});

Notification.belongsTo(models.Vault, {
foreignKey: 'vault_id',
as: 'vault',
});

Notification.belongsTo(models.SubSchedule, {
foreignKey: 'sub_schedule_id',
as: 'subSchedule',
});
};

module.exports = {
Vault,
SubSchedule,
Beneficiary,
Organization,
Notification,
};
8 changes: 8 additions & 0 deletions backend/src/models/beneficiary.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ const Beneficiary = sequelize.define('Beneficiary', {
allowNull: false,
comment: 'Beneficiary wallet address',
},
email: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Beneficiary email address',
validate: {
isEmail: true,
},
},
total_allocated: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const SubSchedule = require('./subSchedule');
const TVL = require('./tvl');
const Beneficiary = require('./beneficiary');
const Organization = require('./organization');
const Notification = require('./notification');

const { Token, initTokenModel } = require('./token');
const { OrganizationWebhook, initOrganizationWebhookModel } = require('./organizationWebhook');
Expand All @@ -23,8 +24,7 @@ const models = {
TVL,
Beneficiary,
Organization,
Token,
OrganizationWebhook,

sequelize,
};

Expand Down
62 changes: 62 additions & 0 deletions backend/src/models/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../database/connection');

const Notification = sequelize.define('Notification', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
beneficiary_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'beneficiaries',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
vault_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'vaults',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
sub_schedule_id: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'sub_schedules',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
type: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Type of notification, e.g., CLIFF_PASSED',
},
sent_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
}, {
tableName: 'notifications',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['beneficiary_id', 'vault_id', 'sub_schedule_id', 'type'],
unique: true,
},
],
});

module.exports = Notification;
30 changes: 26 additions & 4 deletions backend/src/models/subSchedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,34 @@ const SubSchedule = sequelize.define('SubSchedule', {
allowNull: false,
comment: 'Amount of tokens added in this top-up',
},
created_at: {

type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
allowNull: false,
},
updated_at: {
cliff_duration: {
type: DataTypes.INTEGER,
allowNull: true,
},
cliff_date: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
allowNull: true,
},
vesting_start_date: {
type: DataTypes.DATE,
allowNull: false,
},
vesting_duration: {
type: DataTypes.INTEGER,
allowNull: false,
},
amount_released: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
defaultValue: 0,
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
}, {
tableName: 'sub_schedules',
Expand All @@ -39,6 +60,7 @@ const SubSchedule = sequelize.define('SubSchedule', {
{
fields: ['vault_id'],
},

],
});

Expand Down
66 changes: 66 additions & 0 deletions backend/src/services/emailService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
const nodemailer = require('nodemailer');

class EmailService {
constructor() {
this.transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST || 'smtp.mailtrap.io',
port: process.env.EMAIL_PORT || 2525,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS,
},
});
}

/**
* Send an email
* @param {string} to - Recipient email
* @param {string} subject - Email subject
* @param {string} text - Email body (plain text)
* @param {string} html - Email body (HTML)
* @returns {Promise<boolean>} Success status
*/
async sendEmail(to, subject, text, html) {
try {
if (!to) {
console.warn('No recipient email provided, skipping email notification');
return false;
}

if (!process.env.EMAIL_USER || !process.env.EMAIL_PASS) {
console.warn('Email credentials not set, skipping email notification');
return false;
}

const info = await this.transporter.sendMail({
from: `"Vesting Vault" <${process.env.EMAIL_FROM || 'no-reply@vestingvault.com'}>`,
to,
subject,
text,
html,
});

console.log('Email sent: %s', info.messageId);
return true;
} catch (error) {
console.error('Error sending email:', error.message);
return false;
}
}

/**
* Send cliff passed notification
* @param {string} to - Recipient email
* @param {string} amount - Claimable amount
* @returns {Promise<boolean>} Success status
*/
async sendCliffPassedEmail(to, amount) {
const subject = 'Your Cliff has passed!';
const text = `Your Cliff has passed! You can now claim ${parseFloat(amount).toLocaleString()} tokens.`;
const html = `<p>Your Cliff has passed! You can now claim <strong>${parseFloat(amount).toLocaleString()}</strong> tokens.</p>`;

return await this.sendEmail(to, subject, text, html);
}
}

module.exports = new EmailService();
Loading