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
15 changes: 0 additions & 15 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +0,0 @@
DATABASE_URL=postgresql://username:password@localhost:5432/vesting_vault
DB_HOST=localhost
DB_PORT=5432
DB_NAME=vesting_vault
DB_USER=username
DB_PASSWORD=password

STELLAR_RPC_URL=https://horizon-testnet.stellar.org
STELLAR_NETWORK=testnet
SOROBAN_RPC_URL=https://soroban-rpc.testnet.stellar.org
VAULT_CONTRACT_ADDRESS=CD5QF6KBAURVUNZR2EVBJISWSEYGDGEEYVH2XYJJADKT7KFOXTTIXLHU

APPROVED_VAULT_WASM_HASH=7792a624b562b3d9414792f5fb5d72f53b9838fef2ed9a901471253970bc3b15

PORT=3000
28 changes: 2 additions & 26 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,10 @@ name: Vesting Vault Tests

on:
push:
branches: [ "main" ]
branches: [ "main", "develop" ]
pull_request:
branches: [ "main" ]
branches: [ "main", "develop" ]

jobs:
test:
runs-on: ubuntu-latest

defaults:
run:
working-directory: backend

env:
NODE_ENV: test

steps:
- uses: actions/checkout@v4

- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: backend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

60 changes: 0 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,60 +0,0 @@
# Verinode Vesting Vault System

A blockchain-based vesting vault system for managing token distributions and vesting schedules with cliff support for top-ups.

## API Documentation

The API is fully documented with Swagger UI. After starting the server, access the documentation at:

```
http://localhost:3000/api-docs
```

The documentation includes:
- Interactive API explorer for all endpoints
- Detailed parameter descriptions
- Example requests and responses
- Authentication information
- Model definitions

## Quick Start

Get the entire development environment running in minutes:

```bash
# Clone and start
git clone <repository-url>
cd Vesting-Vault
docker-compose up -d

# Verify it's working
curl http://localhost:3000/health
```

## Services

- **Backend API**: http://localhost:3000
- **PostgreSQL Database**: localhost:5432
- **Redis Cache**: localhost:6379

## Development

See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed development setup and guidelines.

## Architecture

- **Backend**: Node.js with Express and Sequelize ORM
- **Database**: PostgreSQL for persistent storage
- **Cache**: Redis for session management and caching
- **Containerization**: Docker and Docker Compose for development environment

## Features

- **Vesting Schedules**: Flexible vesting with cliff periods and multiple top-ups
- **Admin Management**: Secure admin key management and audit logging
- **Price Tracking**: Historical price tracking for tax reporting
- **Delegate Claiming**: Allow beneficiaries to set delegates to claim on their behalf ([docs](./DELEGATE_CLAIMING.md))

## License

MIT
89 changes: 2 additions & 87 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,9 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const { Client } = require('pg');
const { Server } = require('stellar-sdk');
const { validateNetworkOnStartup } = require('./backend/src/jobs/networkValidation');


const app = express();
const port = process.env.PORT || 3000;

app.use(cors());

// Health check endpoint
app.get('/health', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
services: {
database: 'unknown',
stellar: 'unknown'
}
};

let allHealthy = true;

// Check database connection
try {
const client = new Client({
connectionString: process.env.DATABASE_URL || {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD
}
});

await client.connect();
await client.query('SELECT 1');
await client.end();
health.services.database = 'healthy';
} catch (error) {
health.services.database = 'unhealthy';
health.database_error = error.message;
allHealthy = false;
}

// Check Stellar RPC connection
try {
if (!process.env.STELLAR_RPC_URL) {
throw new Error('STELLAR_RPC_URL is not configured');
}
const stellarServer = new Server(process.env.STELLAR_RPC_URL);
await stellarServer.root();
health.services.stellar = 'healthy';
} catch (error) {
health.services.stellar = 'unhealthy';
health.stellar_error = error.message;
allHealthy = false;
}

if (allHealthy) {
res.status(200).json(health);
} else {
health.status = 'degraded';
res.status(503).json(health);
}
});

// Middleware
app.use(cors());
app.use(express.json());

app.get('/', (req, res) => {
const dbStatus = dbManager.getStatus();
res.json({
project: 'Vesting Vault',
status: 'Tracking Locked Tokens',
contract: process.env.VAULT_CONTRACT_ADDRESS,
database: dbStatus
});
});

async function startServer() {
try {
await validateNetworkOnStartup();
app.listen(port, () => console.log(`Vesting API running on port ${port}`));
} catch (error) {
console.error('\n❌ Fatal Startup Error:', error.message, '\n');
process.exit(1);
}
}

startServer();
15 changes: 15 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
testEnvironment: 'node',
testMatch: ['**/tests/**/*.test.js'],
collectCoverageFrom: [
'models/**/*.js',
'services/**/*.js',
'middleware/**/*.js',
'routes/**/*.js',
'!**/node_modules/**'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
testTimeout: 10000
};
125 changes: 125 additions & 0 deletions middleware/auditMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const AuditService = require('../services/AuditService');

class AuditMiddleware {
constructor() {
this.auditService = new AuditService();
}

auditAction(actionType, getActorId = (req) => req.user?.id || req.ip, getTargetId = null, getOldData = null, getNewData = null) {
return async (req, res, next) => {
const originalSend = res.send;
let responseData = null;
let capturedError = null;

res.send = function(data) {
responseData = data;
return originalSend.call(this, data);
};

const originalJson = res.json;
res.json = function(data) {
responseData = data;
return originalJson.call(this, data);
};

res.on('finish', async () => {
try {
if (res.statusCode >= 200 && res.statusCode < 300) {
const actorId = getActorId(req);
const targetId = getTargetId ? getTargetId(req, res) : null;
const oldData = getOldData ? getOldData(req, res) : null;
const newData = getNewData ? getNewData(req, res) : null;

const metadata = {
method: req.method,
url: req.originalUrl,
userAgent: req.get('User-Agent'),
ip: req.ip,
statusCode: res.statusCode,
timestamp: new Date().toISOString()
};

await this.auditService.logAdminAction(
actionType,
actorId,
targetId,
oldData,
newData,
metadata
);
}
} catch (error) {
console.error('Error in audit middleware:', error);
}
}.bind(this));

next();
};
}

auditVestingChanges() {
return this.auditAction(
'VESTING_CHANGE',
(req) => req.user?.id || req.body.adminId || 'unknown',
(req) => req.params.beneficiaryId || req.body.beneficiaryId,
(req) => req.body.previousData,
(req) => req.body.newData
);
}

auditCliffDateChanges() {
return this.auditAction(
'CLIFF_DATE_CHANGE',
(req) => req.user?.id || req.body.adminId || 'unknown',
(req) => req.params.beneficiaryId || req.body.beneficiaryId,
(req) => ({ previousCliffDate: req.body.previousCliffDate }),
(req) => ({ newCliffDate: req.body.newCliffDate })
);
}

auditBeneficiaryChanges() {
return this.auditAction(
'BENEFICIARY_CHANGE',
(req) => req.user?.id || req.body.adminId || 'unknown',
(req) => req.params.beneficiaryId || req.body.beneficiaryId,
(req) => req.body.previousBeneficiaryData,
(req) => req.body.newBeneficiaryData
);
}

auditAdminActions() {
return this.auditAction(
'ADMIN_ACTION',
(req) => req.user?.id || req.body.adminId || 'unknown',
(req) => req.params.id || req.body.targetId,
null,
(req) => ({
action: req.body.action,
changes: req.body.changes,
requestBody: req.body
})
);
}

manualAudit(actionType, actorId, targetId = null, oldData = null, newData = null, metadata = null) {
return async (req, res, next) => {
try {
await this.auditService.logAdminAction(
actionType,
actorId,
targetId,
oldData,
newData,
metadata
);

res.json({ success: true, message: 'Audit log created successfully' });
} catch (error) {
console.error('Error in manual audit:', error);
res.status(500).json({ error: 'Failed to create audit log' });
}
};
}
}

module.exports = new AuditMiddleware();
Loading