Skip to content

Commit f331a3b

Browse files
authored
Merge pull request #211 from JamesEjembi/feature/institutional-grade-upgrades
feat: Institutional Reliability and Governance Suite (#97, #98, #99, #100)
2 parents 389802a + 34a4bfc commit f331a3b

11 files changed

Lines changed: 636 additions & 74 deletions

backend/src/models/daoProposal.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const { DataTypes } = require('sequelize');
2+
const { sequelize } = require('../database/connection');
3+
4+
const DAOProposal = sequelize.define('DAOProposal', {
5+
id: {
6+
type: DataTypes.UUID,
7+
defaultValue: DataTypes.UUIDV4,
8+
primaryKey: true,
9+
},
10+
project_id: {
11+
type: DataTypes.INTEGER,
12+
// Note: References should be the table name, not the model name
13+
references: {
14+
model: 'grant_streams',
15+
key: 'id',
16+
},
17+
},
18+
title: {
19+
type: DataTypes.STRING,
20+
allowNull: false,
21+
},
22+
status: {
23+
type: DataTypes.ENUM('active', 'completed', 'failed', 'cancelled'),
24+
defaultValue: 'active',
25+
},
26+
outcome_success: {
27+
type: DataTypes.BOOLEAN,
28+
defaultValue: false,
29+
comment: 'Whether the project successfully achieved its milestones',
30+
},
31+
}, {
32+
tableName: 'dao_proposals',
33+
timestamps: true,
34+
});
35+
36+
DAOProposal.associate = (models) => {
37+
DAOProposal.belongsTo(models.GrantStream, { foreignKey: 'project_id', as: 'project' });
38+
DAOProposal.hasMany(models.DAOVote, { foreignKey: 'proposal_id', as: 'votes' });
39+
};
40+
41+
module.exports = DAOProposal;

backend/src/models/daoVote.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { DataTypes } = require('sequelize');
2+
const { sequelize } = require('../database/connection');
3+
4+
const DAOVote = sequelize.define('DAOVote', {
5+
id: {
6+
type: DataTypes.UUID,
7+
defaultValue: DataTypes.UUIDV4,
8+
primaryKey: true,
9+
},
10+
proposal_id: {
11+
type: DataTypes.UUID,
12+
allowNull: false,
13+
references: {
14+
model: 'dao_proposals',
15+
key: 'id',
16+
},
17+
},
18+
voter_address: {
19+
type: DataTypes.STRING,
20+
allowNull: false,
21+
comment: 'Wallet address of the voter',
22+
},
23+
vote_outcome: {
24+
type: DataTypes.BOOLEAN,
25+
allowNull: false,
26+
comment: 'True for YES, False for NO',
27+
},
28+
vote_weight: {
29+
type: DataTypes.DECIMAL(36, 18),
30+
allowNull: false,
31+
defaultValue: 1.0,
32+
},
33+
}, {
34+
tableName: 'dao_votes',
35+
timestamps: true,
36+
indexes: [
37+
{ fields: ['voter_address'] },
38+
{ fields: ['proposal_id'] },
39+
],
40+
});
41+
42+
DAOVote.associate = (models) => {
43+
DAOVote.belongsTo(models.DAOProposal, { foreignKey: 'proposal_id', as: 'proposal' });
44+
};
45+
46+
module.exports = DAOVote;

backend/src/models/grantStream.js

Lines changed: 83 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,86 @@
11
const { DataTypes } = require('sequelize');
2+
const { sequelize } = require('../database/connection');
23

3-
module.exports = (sequelize) => {
4-
const GrantStream = sequelize.define('GrantStream', {
5-
id: {
6-
type: DataTypes.INTEGER,
7-
primaryKey: true,
8-
autoIncrement: true,
9-
},
10-
address: {
11-
type: DataTypes.STRING(42),
12-
allowNull: false,
13-
unique: true,
14-
comment: 'Contract address of the grant stream',
15-
},
16-
name: {
17-
type: DataTypes.STRING(255),
18-
allowNull: false,
19-
comment: 'Human-readable name of the grant project',
20-
},
21-
description: {
22-
type: DataTypes.TEXT,
23-
comment: 'Detailed description of the grant project',
24-
},
25-
owner_address: {
26-
type: DataTypes.STRING(42),
27-
allowNull: false,
28-
comment: 'Address of the grant project owner',
29-
},
30-
token_address: {
31-
type: DataTypes.STRING(42),
32-
allowNull: false,
33-
comment: 'Token address used for funding',
34-
},
35-
target_amount: {
36-
type: DataTypes.DECIMAL(20, 8),
37-
defaultValue: 0,
38-
comment: 'Target funding amount for the grant',
39-
},
40-
current_amount: {
41-
type: DataTypes.DECIMAL(20, 8),
42-
defaultValue: 0,
43-
comment: 'Current amount funded to the grant',
44-
},
45-
is_active: {
46-
type: DataTypes.BOOLEAN,
47-
defaultValue: true,
48-
comment: 'Whether the grant stream is currently active',
49-
},
50-
start_date: {
51-
type: DataTypes.DATE,
52-
defaultValue: DataTypes.NOW,
53-
comment: 'When the grant stream starts accepting funds',
54-
},
55-
end_date: {
56-
type: DataTypes.DATE,
57-
comment: 'When the grant stream stops accepting funds',
58-
},
59-
metadata: {
60-
type: DataTypes.JSONB,
61-
defaultValue: {},
62-
comment: 'Additional project metadata',
63-
},
64-
}, {
65-
tableName: 'grant_streams',
66-
timestamps: true,
67-
createdAt: 'created_at',
68-
updatedAt: 'updated_at',
69-
indexes: [
70-
{ fields: ['address'] },
71-
{ fields: ['is_active'] },
72-
{ fields: ['owner_address'] },
73-
],
74-
});
4+
const GrantStream = sequelize.define('GrantStream', {
5+
id: {
6+
type: DataTypes.INTEGER,
7+
primaryKey: true,
8+
autoIncrement: true,
9+
},
10+
address: {
11+
type: DataTypes.STRING(42),
12+
allowNull: false,
13+
unique: true,
14+
comment: 'Contract address of the grant stream',
15+
},
16+
name: {
17+
type: DataTypes.STRING(255),
18+
allowNull: false,
19+
comment: 'Human-readable name of the grant project',
20+
},
21+
description: {
22+
type: DataTypes.TEXT,
23+
comment: 'Detailed description of the grant project',
24+
},
25+
owner_address: {
26+
type: DataTypes.STRING(42),
27+
allowNull: false,
28+
comment: 'Address of the grant project owner',
29+
},
30+
token_address: {
31+
type: DataTypes.STRING(42),
32+
allowNull: false,
33+
comment: 'Token address used for funding',
34+
},
35+
target_amount: {
36+
type: DataTypes.DECIMAL(20, 8),
37+
defaultValue: 0,
38+
comment: 'Target funding amount for the grant',
39+
},
40+
current_amount: {
41+
type: DataTypes.DECIMAL(20, 8),
42+
defaultValue: 0,
43+
comment: 'Current amount funded to the grant',
44+
},
45+
is_active: {
46+
type: DataTypes.BOOLEAN,
47+
defaultValue: true,
48+
comment: 'Whether the grant stream is currently active',
49+
},
50+
start_date: {
51+
type: DataTypes.DATE,
52+
defaultValue: DataTypes.NOW,
53+
comment: 'When the grant stream starts accepting funds',
54+
},
55+
end_date: {
56+
type: DataTypes.DATE,
57+
comment: 'When the grant stream stops accepting funds',
58+
},
59+
metadata: {
60+
type: DataTypes.JSONB,
61+
defaultValue: {},
62+
comment: 'Additional project metadata',
63+
},
64+
backup_wallet: {
65+
type: DataTypes.STRING(56),
66+
allowNull: true,
67+
comment: 'Nominated backup wallet for succession',
68+
},
69+
last_active_at: {
70+
type: DataTypes.DATE,
71+
defaultValue: DataTypes.NOW,
72+
comment: 'Timestamp of the last action by the primary wallet',
73+
},
74+
}, {
75+
tableName: 'grant_streams',
76+
timestamps: true,
77+
createdAt: 'created_at',
78+
updatedAt: 'updated_at',
79+
indexes: [
80+
{ fields: ['address'] },
81+
{ fields: ['is_active'] },
82+
{ fields: ['owner_address'] },
83+
],
84+
});
7585

76-
return GrantStream;
77-
};
86+
module.exports = GrantStream;

backend/src/models/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ const GrantStream = require("./grantStream");
3737
const FutureLien = require("./futureLien");
3838
const LienRelease = require("./lienRelease");
3939
const LienMilestone = require("./lienMilestone");
40+
const DAOProposal = require("./daoProposal");
41+
const DAOVote = require("./daoVote");
42+
const ContractUpgradeSignature = require("./contractUpgradeSignature");
43+
const ContractUpgradeAuditLog = require("./contractUpgradeAuditLog");
4044

4145
const { Token, initTokenModel } = require("./token");
4246
const {
@@ -90,6 +94,10 @@ const models = {
9094
FutureLien,
9195
LienRelease,
9296
LienMilestone,
97+
DAOProposal,
98+
DAOVote,
99+
ContractUpgradeSignature,
100+
ContractUpgradeAuditLog,
93101
sequelize,
94102
};
95103

backend/src/models/vault.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ const Vault = sequelize.define('Vault', {
6464
defaultValue: 0,
6565

6666
},
67+
accumulated_fees: {
68+
type: DataTypes.DECIMAL(36, 18),
69+
allowNull: false,
70+
defaultValue: 0,
71+
comment: 'Sustainability fees accumulated in this vault',
72+
},
6773

6874
token_type: {
6975
type: DataTypes.ENUM('static', 'dynamic'),
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
'use strict';
2+
3+
const { Vault } = require('../models');
4+
const { sequelize } = require('../database/connection');
5+
const { Op } = require('sequelize');
6+
7+
class FeeDistributorService {
8+
/**
9+
* Track accumulated fees across all vaults.
10+
* This is a batch process to summarize accumulated fees and trigger distribution.
11+
*/
12+
async summarizeAccumulatedFees() {
13+
const totalFees = await Vault.sum('accumulated_fees', {
14+
where: {
15+
accumulated_fees: { [Op.gt]: 0 }
16+
}
17+
});
18+
19+
return totalFees || 0;
20+
}
21+
22+
/**
23+
* Check if the total accumulated fees across all vaults meet the threshold.
24+
* If so, trigger a batch transaction (simulated for now).
25+
*/
26+
async checkAndDistributeFees() {
27+
const threshold = parseFloat(process.env.PROTOCOL_FEE_THRESHOLD || '100'); // Default 100 tokens
28+
const total = await this.summarizeAccumulatedFees();
29+
30+
if (total >= threshold) {
31+
console.log(`🚀 Fee threshold met (${total} >= ${threshold}). Triggering distribution to Treasury...`);
32+
return await this.distributeFees();
33+
}
34+
35+
return { success: true, message: 'Threshold not met yet', currentTotal: total };
36+
}
37+
38+
/**
39+
* Distribute fees by moving them to the treasury.
40+
*/
41+
async distributeFees() {
42+
const treasuryAddress = process.env.PROTOCOL_TREASURY_ADDRESS;
43+
if (!treasuryAddress) throw new Error('PROTOCOL_TREASURY_ADDRESS not configured');
44+
45+
const vaultsWithFees = await Vault.findAll({
46+
where: {
47+
accumulated_fees: { [Op.gt]: 0 }
48+
}
49+
});
50+
51+
const distributions = [];
52+
53+
// Use transaction for consistency
54+
const result = await sequelize.transaction(async (t) => {
55+
for (const vault of vaultsWithFees) {
56+
const amount = vault.accumulated_fees;
57+
58+
// Simulation: In reality, we'd trigger a Stellar transaction here via StellarService
59+
distributions.push({
60+
vault_address: vault.address,
61+
amount: amount,
62+
token_address: vault.token_address,
63+
recipient: treasuryAddress
64+
});
65+
66+
// Reset accumulated fees in this vault
67+
await vault.update({ accumulated_fees: 0 }, { transaction: t });
68+
}
69+
return distributions;
70+
});
71+
72+
return {
73+
success: true,
74+
total_distributed: distributions.reduce((sum, d) => sum + parseFloat(d.amount), 0),
75+
distributions
76+
};
77+
}
78+
79+
/**
80+
* Helper to add fees to a vault (e.g., when a stream is processed).
81+
* Usually called by another service when a distribution or payment is made.
82+
*/
83+
async accumulateFeeForVault(vaultId, transactionAmount) {
84+
const feeRate = parseFloat(process.env.PROTOCOL_FEE_RATE || '0.001'); // 0.1%
85+
const feeAmount = transactionAmount * feeRate;
86+
87+
const vault = await Vault.findByPk(vaultId);
88+
if (vault) {
89+
await vault.update({
90+
accumulated_fees: parseFloat(vault.accumulated_fees) + feeAmount
91+
});
92+
}
93+
return feeAmount;
94+
}
95+
}
96+
97+
module.exports = new FeeDistributorService();

0 commit comments

Comments
 (0)