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
44 changes: 42 additions & 2 deletions backend/src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import { Router } from 'express'
import { Router, Request, Response } from 'express'
import { StellarService } from '../services/stellar.js'
import { ReflectorService } from '../services/reflector.js'
import { RebalanceHistoryService } from '../services/rebalanceHistory.js'
Expand All @@ -11,7 +11,47 @@ import { notificationService } from '../services/notificationService.js'
import { contractEventIndexerService } from '../services/contractEventIndexer.js'
import { logger } from '../utils/logger.js'
import { idempotencyMiddleware } from '../middleware/idempotency.js'
import { requireAdmin } from '../middleware/auth.js'
import { writeRateLimiter } from '../middleware/rateLimit.js'
import { getQueueMetrics } from '../queue/queueMetrics.js'
import { blockDebugInProduction } from '../middleware/debugGate.js'
import { getFeatureFlags, getPublicFeatureFlags } from '../config/featureFlags.js'
import { autoRebalancer } from '../index.js'

const stellarService = new StellarService()
const reflectorService = new ReflectorService()
const rebalanceHistoryService = new RebalanceHistoryService()
const riskManagementService = new RiskManagementService()

const featureFlags = getFeatureFlags()
const publicFeatureFlags = getPublicFeatureFlags()

const router = Router()

const getErrorMessage = (error: unknown): string => {
if (error instanceof Error) return error.message;
return String(error);
}

const getErrorObject = (error: unknown): Error => {
if (error instanceof Error) return error;
return new Error(String(error));
}

const parseOptionalBoolean = (value: unknown): boolean | undefined => {
if (typeof value === 'boolean') return value;
if (value === 'true') return true;
if (value === 'false') return false;
return undefined;
}

function getPortfolioAllocationsAsRecord(portfolio: any): Record<string, number> {
if (!portfolio || !portfolio.targetAllocations) return {};
if (portfolio.targetAllocations instanceof Map) {
return Object.fromEntries(portfolio.targetAllocations);
}
return portfolio.targetAllocations;
}
const parseOptionalTimestamp = (value: unknown): string | undefined => {
if (value === undefined || value === null || value === '') return undefined
if (typeof value !== 'string') return undefined
Expand Down Expand Up @@ -48,7 +88,7 @@ router.get('/rebalance/history', async (req, res) => {
portfolioId || undefined,
limit,
{
eventSource: source,
eventSource: source === 'all' ? undefined : source,
startTimestamp,
endTimestamp
}
Expand Down
18 changes: 10 additions & 8 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,14 @@ impl PortfolioRebalancer {
.unwrap()
}

pub fn deposit(env: Env, portfolio_id: u64, asset: Address, amount: i128) {
pub fn deposit(env: Env, portfolio_id: u64, asset: Address, amount: i128) -> Result<(), Error> {
if amount <= 0 {
panic!("Amount must be positive");
return Err(Error::InvalidAmount);
}

// Check for emergency stop
if let Some(true) = env.storage().instance().get(&DataKey::EmergencyStop) {
panic!("Emergency stop active");
return Err(Error::EmergencyStop);
}

let mut portfolio: Portfolio = env.storage().persistent()
Expand All @@ -97,6 +97,7 @@ impl PortfolioRebalancer {
("portfolio", "deposit"),
(portfolio_id, asset, amount)
);
Ok(())
}

pub fn check_rebalance_needed(env: Env, portfolio_id: u64) -> bool {
Expand Down Expand Up @@ -136,10 +137,10 @@ impl PortfolioRebalancer {
false
}

pub fn execute_rebalance(env: Env, portfolio_id: u64) {
pub fn execute_rebalance(env: Env, portfolio_id: u64) -> Result<(), Error> {
// Check for emergency stop
if let Some(true) = env.storage().instance().get(&DataKey::EmergencyStop) {
panic!("Emergency stop active");
return Err(Error::EmergencyStop);
}

let mut portfolio: Portfolio = env.storage().persistent()
Expand All @@ -151,7 +152,7 @@ impl PortfolioRebalancer {
// Check cooldown (e.g., 1 hour = 3600 seconds)
let current_time = env.ledger().timestamp();
if current_time < portfolio.last_rebalance + 3600 {
panic!("Cooldown active");
return Err(Error::CooldownActive);
}

// Reflector check for stale data
Expand All @@ -162,11 +163,11 @@ impl PortfolioRebalancer {
for (asset, _) in portfolio.target_allocations.iter() {
if let Some(price_data) = reflector_client.lastprice(&crate::reflector::Asset::Stellar(asset.clone())) {
if price_data.is_stale(current_time, 3600) {
panic!("Stale price data");
return Err(Error::StaleData);
}
} else {
// If price is missing, we can't safely rebalance
panic!("Missing price data");
return Err(Error::MissingPriceData);
}
}

Expand All @@ -179,6 +180,7 @@ impl PortfolioRebalancer {
("portfolio", "rebalanced"),
(portfolio_id, current_time)
);
Ok(())
}

pub fn set_emergency_stop(env: Env, stop: bool) {
Expand Down
20 changes: 9 additions & 11 deletions contracts/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ fn test_deposit_valid() {
}

#[test]
#[should_panic(expected = "Amount must be positive")]
fn test_deposit_invalid_amount() {
let env = Env::default();
env.mock_all_auths();
Expand All @@ -116,7 +115,8 @@ fn test_deposit_invalid_amount() {
allocations.set(asset.clone(), 100);
let pid = client.create_portfolio(&user, &allocations, &5);

client.deposit(&pid, &asset, &0);
let result = client.try_deposit(&pid, &asset, &0);
assert_eq!(result, Err(Ok(Error::InvalidAmount)));
}

#[test]
Expand Down Expand Up @@ -222,7 +222,6 @@ fn test_execute_rebalance_success() {
}

#[test]
#[should_panic(expected = "Cooldown active")]
fn test_execute_rebalance_cooldown() {
let env = Env::default();
env.mock_all_auths();
Expand All @@ -249,11 +248,11 @@ fn test_execute_rebalance_cooldown() {
li.timestamp = 10010;
});

client.execute_rebalance(&pid);
let result = client.try_execute_rebalance(&pid);
assert_eq!(result, Err(Ok(Error::CooldownActive)));
}

#[test]
#[should_panic(expected = "Emergency stop active")]
fn test_emergency_stop() {
let env = Env::default();
env.mock_all_auths();
Expand All @@ -271,15 +270,13 @@ fn test_emergency_stop() {
let asset = Address::generate(&env);
allocations.set(asset.clone(), 100);

// Try deposit (should panic)
// Note: creating portfolio might work depending on implementation,
// but deposit/rebalance should fail. Validating deposit fail here.
// Try deposit (should return error)
let pid = client.create_portfolio(&user, &allocations, &5);
client.deposit(&pid, &asset, &100);
let result = client.try_deposit(&pid, &asset, &100);
assert_eq!(result, Err(Ok(Error::EmergencyStop)));
}

#[test]
#[should_panic(expected = "Stale price data")]
fn test_stale_data() {
let env = Env::default();
env.mock_all_auths();
Expand Down Expand Up @@ -342,7 +339,8 @@ fn test_stale_data() {
li.timestamp = 20000;
});

client.execute_rebalance(&pid);
let result = client.try_execute_rebalance(&pid);
assert_eq!(result, Err(Ok(Error::StaleData)));
}

#[test]
Expand Down
2 changes: 2 additions & 0 deletions contracts/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ pub enum Error {
ExcessiveDrift = 6,
AlreadyInitialized = 7,
InvalidThreshold = 8,
InvalidAmount = 9,
MissingPriceData = 10,
}
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,36 @@
},
"failed_call": false
},
{
"event": {
"ext": "v0",
"contract_id": "0000000000000000000000000000000000000000000000000000000000000001",
"type_": "contract",
"body": {
"v0": {
"topics": [
{
"string": "portfolio"
},
{
"string": "created"
}
],
"data": {
"vec": [
{
"u64": 0
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4"
}
]
}
}
}
},
"failed_call": false
},
{
"event": {
"ext": "v0",
Expand Down Expand Up @@ -653,6 +683,42 @@
},
"failed_call": false
},
{
"event": {
"ext": "v0",
"contract_id": "0000000000000000000000000000000000000000000000000000000000000001",
"type_": "contract",
"body": {
"v0": {
"topics": [
{
"string": "portfolio"
},
{
"string": "deposit"
}
],
"data": {
"vec": [
{
"u64": 0
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM"
},
{
"i128": {
"hi": 0,
"lo": 100
}
}
]
}
}
}
},
"failed_call": false
},
{
"event": {
"ext": "v0",
Expand Down Expand Up @@ -713,6 +779,42 @@
},
"failed_call": false
},
{
"event": {
"ext": "v0",
"contract_id": "0000000000000000000000000000000000000000000000000000000000000001",
"type_": "contract",
"body": {
"v0": {
"topics": [
{
"string": "portfolio"
},
{
"string": "deposit"
}
],
"data": {
"vec": [
{
"u64": 0
},
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDR4"
},
{
"i128": {
"hi": 0,
"lo": 100
}
}
]
}
}
}
},
"failed_call": false
},
{
"event": {
"ext": "v0",
Expand Down
Loading
Loading