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
41 changes: 41 additions & 0 deletions backend/src/models/daoProposal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../database/connection');

const DAOProposal = sequelize.define('DAOProposal', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
project_id: {
type: DataTypes.INTEGER,
// Note: References should be the table name, not the model name
references: {
model: 'grant_streams',
key: 'id',
},
},
title: {
type: DataTypes.STRING,
allowNull: false,
},
status: {
type: DataTypes.ENUM('active', 'completed', 'failed', 'cancelled'),
defaultValue: 'active',
},
outcome_success: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: 'Whether the project successfully achieved its milestones',
},
}, {
tableName: 'dao_proposals',
timestamps: true,
});

DAOProposal.associate = (models) => {
DAOProposal.belongsTo(models.GrantStream, { foreignKey: 'project_id', as: 'project' });
DAOProposal.hasMany(models.DAOVote, { foreignKey: 'proposal_id', as: 'votes' });
};

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

const DAOVote = sequelize.define('DAOVote', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
proposal_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'dao_proposals',
key: 'id',
},
},
voter_address: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Wallet address of the voter',
},
vote_outcome: {
type: DataTypes.BOOLEAN,
allowNull: false,
comment: 'True for YES, False for NO',
},
vote_weight: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
defaultValue: 1.0,
},
}, {
tableName: 'dao_votes',
timestamps: true,
indexes: [
{ fields: ['voter_address'] },
{ fields: ['proposal_id'] },
],
});

DAOVote.associate = (models) => {
DAOVote.belongsTo(models.DAOProposal, { foreignKey: 'proposal_id', as: 'proposal' });
};

module.exports = DAOVote;
157 changes: 83 additions & 74 deletions backend/src/models/grantStream.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,86 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../database/connection');

module.exports = (sequelize) => {
const GrantStream = sequelize.define('GrantStream', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
address: {
type: DataTypes.STRING(42),
allowNull: false,
unique: true,
comment: 'Contract address of the grant stream',
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
comment: 'Human-readable name of the grant project',
},
description: {
type: DataTypes.TEXT,
comment: 'Detailed description of the grant project',
},
owner_address: {
type: DataTypes.STRING(42),
allowNull: false,
comment: 'Address of the grant project owner',
},
token_address: {
type: DataTypes.STRING(42),
allowNull: false,
comment: 'Token address used for funding',
},
target_amount: {
type: DataTypes.DECIMAL(20, 8),
defaultValue: 0,
comment: 'Target funding amount for the grant',
},
current_amount: {
type: DataTypes.DECIMAL(20, 8),
defaultValue: 0,
comment: 'Current amount funded to the grant',
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether the grant stream is currently active',
},
start_date: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'When the grant stream starts accepting funds',
},
end_date: {
type: DataTypes.DATE,
comment: 'When the grant stream stops accepting funds',
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
comment: 'Additional project metadata',
},
}, {
tableName: 'grant_streams',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{ fields: ['address'] },
{ fields: ['is_active'] },
{ fields: ['owner_address'] },
],
});
const GrantStream = sequelize.define('GrantStream', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
address: {
type: DataTypes.STRING(42),
allowNull: false,
unique: true,
comment: 'Contract address of the grant stream',
},
name: {
type: DataTypes.STRING(255),
allowNull: false,
comment: 'Human-readable name of the grant project',
},
description: {
type: DataTypes.TEXT,
comment: 'Detailed description of the grant project',
},
owner_address: {
type: DataTypes.STRING(42),
allowNull: false,
comment: 'Address of the grant project owner',
},
token_address: {
type: DataTypes.STRING(42),
allowNull: false,
comment: 'Token address used for funding',
},
target_amount: {
type: DataTypes.DECIMAL(20, 8),
defaultValue: 0,
comment: 'Target funding amount for the grant',
},
current_amount: {
type: DataTypes.DECIMAL(20, 8),
defaultValue: 0,
comment: 'Current amount funded to the grant',
},
is_active: {
type: DataTypes.BOOLEAN,
defaultValue: true,
comment: 'Whether the grant stream is currently active',
},
start_date: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'When the grant stream starts accepting funds',
},
end_date: {
type: DataTypes.DATE,
comment: 'When the grant stream stops accepting funds',
},
metadata: {
type: DataTypes.JSONB,
defaultValue: {},
comment: 'Additional project metadata',
},
backup_wallet: {
type: DataTypes.STRING(56),
allowNull: true,
comment: 'Nominated backup wallet for succession',
},
last_active_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
comment: 'Timestamp of the last action by the primary wallet',
},
}, {
tableName: 'grant_streams',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{ fields: ['address'] },
{ fields: ['is_active'] },
{ fields: ['owner_address'] },
],
});

return GrantStream;
};
module.exports = GrantStream;
8 changes: 8 additions & 0 deletions backend/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ const GrantStream = require("./grantStream");
const FutureLien = require("./futureLien");
const LienRelease = require("./lienRelease");
const LienMilestone = require("./lienMilestone");
const DAOProposal = require("./daoProposal");
const DAOVote = require("./daoVote");
const ContractUpgradeSignature = require("./contractUpgradeSignature");
const ContractUpgradeAuditLog = require("./contractUpgradeAuditLog");

const { Token, initTokenModel } = require("./token");
const {
Expand Down Expand Up @@ -90,6 +94,10 @@ const models = {
FutureLien,
LienRelease,
LienMilestone,
DAOProposal,
DAOVote,
ContractUpgradeSignature,
ContractUpgradeAuditLog,
sequelize,
};

Expand Down
6 changes: 6 additions & 0 deletions backend/src/models/vault.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ const Vault = sequelize.define('Vault', {
defaultValue: 0,

},
accumulated_fees: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
defaultValue: 0,
comment: 'Sustainability fees accumulated in this vault',
},

token_type: {
type: DataTypes.ENUM('static', 'dynamic'),
Expand Down
97 changes: 97 additions & 0 deletions backend/src/services/feeDistributorService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use strict';

const { Vault } = require('../models');
const { sequelize } = require('../database/connection');
const { Op } = require('sequelize');

class FeeDistributorService {
/**
* Track accumulated fees across all vaults.
* This is a batch process to summarize accumulated fees and trigger distribution.
*/
async summarizeAccumulatedFees() {
const totalFees = await Vault.sum('accumulated_fees', {
where: {
accumulated_fees: { [Op.gt]: 0 }
}
});

return totalFees || 0;
}

/**
* Check if the total accumulated fees across all vaults meet the threshold.
* If so, trigger a batch transaction (simulated for now).
*/
async checkAndDistributeFees() {
const threshold = parseFloat(process.env.PROTOCOL_FEE_THRESHOLD || '100'); // Default 100 tokens
const total = await this.summarizeAccumulatedFees();

if (total >= threshold) {
console.log(`🚀 Fee threshold met (${total} >= ${threshold}). Triggering distribution to Treasury...`);
return await this.distributeFees();
}

return { success: true, message: 'Threshold not met yet', currentTotal: total };
}

/**
* Distribute fees by moving them to the treasury.
*/
async distributeFees() {
const treasuryAddress = process.env.PROTOCOL_TREASURY_ADDRESS;
if (!treasuryAddress) throw new Error('PROTOCOL_TREASURY_ADDRESS not configured');

const vaultsWithFees = await Vault.findAll({
where: {
accumulated_fees: { [Op.gt]: 0 }
}
});

const distributions = [];

// Use transaction for consistency
const result = await sequelize.transaction(async (t) => {
for (const vault of vaultsWithFees) {
const amount = vault.accumulated_fees;

// Simulation: In reality, we'd trigger a Stellar transaction here via StellarService
distributions.push({
vault_address: vault.address,
amount: amount,
token_address: vault.token_address,
recipient: treasuryAddress
});

// Reset accumulated fees in this vault
await vault.update({ accumulated_fees: 0 }, { transaction: t });
}
return distributions;
});

return {
success: true,
total_distributed: distributions.reduce((sum, d) => sum + parseFloat(d.amount), 0),
distributions
};
}

/**
* Helper to add fees to a vault (e.g., when a stream is processed).
* Usually called by another service when a distribution or payment is made.
*/
async accumulateFeeForVault(vaultId, transactionAmount) {
const feeRate = parseFloat(process.env.PROTOCOL_FEE_RATE || '0.001'); // 0.1%
const feeAmount = transactionAmount * feeRate;

const vault = await Vault.findByPk(vaultId);
if (vault) {
await vault.update({
accumulated_fees: parseFloat(vault.accumulated_fees) + feeAmount
});
}
return feeAmount;
}
}

module.exports = new FeeDistributorService();
Loading
Loading