Skip to content
Open
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
20 changes: 20 additions & 0 deletions backend/migrations/1784000000014_add_loan_events_indexes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 1784000000014_add_loan_events_indexes.js

export async function up(knex) {
await knex.raw(`
-- Index for borrower history queries
CREATE INDEX IF NOT EXISTS loan_events_borrower_idx
ON loan_events (borrower_id);

-- Index for loan status queries
CREATE INDEX IF NOT EXISTS loan_events_status_idx
ON loan_events (loan_id, status);
`);
}

export async function down(knex) {
await knex.raw(`
DROP INDEX IF EXISTS loan_events_borrower_idx;
DROP INDEX IF EXISTS loan_events_status_idx;
`);
}
17 changes: 17 additions & 0 deletions backend/src/db/migrations/20260329_add_loan_events_indexes.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- Migration: Add missing indexes on loan_events table

-- Index for borrower history queries
CREATE INDEX IF NOT EXISTS idx_loan_events_borrower
ON loan_events(borrower);

-- Index for filtering by event type
CREATE INDEX IF NOT EXISTS idx_loan_events_event_type
ON loan_events(event_type);

-- Composite index for loan detail queries
CREATE INDEX IF NOT EXISTS idx_loan_events_loan_id_event_type
ON loan_events(loan_id, event_type);

-- Index for date range filters
CREATE INDEX IF NOT EXISTS idx_loan_events_created_at
ON loan_events(created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Migration: Add unique constraint to prevent duplicate loan events

ALTER TABLE loan_events
ADD CONSTRAINT unique_loan_event
UNIQUE (transaction_hash, event_index);
19 changes: 19 additions & 0 deletions backend/src/services/chainListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// backend/src/services/chainListener.ts

import logger from "../utils/logger.js";

/**
* Subscribe to contract events by contract name.
* In a real implementation, you’d hook into Soroban RPC or your event stream.
*/
export function subscribeToContractEvents(
contractName: string,
handler: (event: any) => Promise<void> | void,
) {
logger.info(`Subscribed to contract events for ${contractName}`);

// Example: you might wire this up to your event stream service
// eventStreamService.on(contractName, handler);

// For now, this is just a placeholder so imports resolve.
}
121 changes: 121 additions & 0 deletions backend/src/services/eventIndexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
} from "./notificationService.js";
import { sorobanService } from "./sorobanService.js";
import { updateUserScoresBulk } from "./scoresService.js";
import { subscribeToContractEvents } from "../services/chainListener.js";
import db from "../db/connection.js";

interface SorobanRawEvent {
id: string;
Expand Down Expand Up @@ -91,6 +93,125 @@ export class EventIndexer {
this.batchSize = configOrRpcUrl.batchSize ?? 100;
}

init() {
// Existing LoanManager subscription...
subscribeToContractEvents('LoanManager', this.handleLoanManagerEvent.bind(this));

// NEW: LendingPool events
subscribeToContractEvents('LendingPool', this.handleLendingPoolEvent.bind(this));

// NEW: RemittanceNFT events
subscribeToContractEvents('RemittanceNFT', this.handleRemittanceNFTEvent.bind(this));

// NEW: MultisigGovernance events
subscribeToContractEvents('MultisigGovernance', this.handleGovernanceEvent.bind(this));
}

async handleLendingPoolEvent(event: any) {
switch (event.type) {
case 'Deposit':
await db('lending_pool_deposits').insert({
userId: event.user,
amount: event.amount,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'Withdraw':
await db('lending_pool_withdrawals').insert({
userId: event.user,
amount: event.amount,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'EmergencyWithdraw':
await db('lending_pool_emergency').insert({
userId: event.user,
amount: event.amount,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
}
}

async handleRemittanceNFTEvent(event: any) {
switch (event.type) {
case 'ScoreUpdated':
await db('nft_scores').insert({
nftId: event.nftId,
newScore: event.score,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'NFTSeized':
await db('nft_seizures').insert({
nftId: event.nftId,
reason: event.reason,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'NFTBurned':
await db('nft_burns').insert({
nftId: event.nftId,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'NFTMinted':
await db('nft_mints').insert({
nftId: event.nftId,
owner: event.owner,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
}
}

async handleGovernanceEvent(event: any) {
switch (event.type) {
case 'ProposalCreated':
await db('governance_proposals').insert({
proposalId: event.proposalId,
creator: event.creator,
expiresAt: event.expiresAt,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'ProposalApproved':
await db('governance_approvals').insert({
proposalId: event.proposalId,
approver: event.approver,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
case 'ProposalFinalized':
await db('governance_finalized').insert({
proposalId: event.proposalId,
executor: event.executor,
txHash: event.txHash,
blockNumber: event.blockNumber,
timestamp: new Date(),
});
break;
}
}

async start(): Promise<void> {
if (this.running) {
logger.warn("Indexer start requested while already running");
Expand Down
123 changes: 123 additions & 0 deletions contracts/multisig_governance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,3 +591,126 @@ impl GovernanceContract {
.expect("contract not initialized (4002)")
}
}

#[derive(scale::Encode, scale::Decode, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub struct Proposal {
pub id: u64,
pub creator: AccountId,
pub approvals: Vec<AccountId>,
pub executed: bool,
pub expires_at: u64, // NEW: ledger height expiry
}

#[ink(storage)]
pub struct MultisigGovernance {
proposals: Mapping<u64, Proposal>,
next_proposal_id: u64,
expiry_window: u64, // NEW: configurable expiry window in ledgers
admin: AccountId,
}

impl MultisigGovernance {
#[ink(constructor)]
pub fn new(admin: AccountId, expiry_window: u64) -> Self {
Self {
proposals: Mapping::default(),
next_proposal_id: 0,
expiry_window,
admin,
}
}

#[ink(message)]
pub fn set_expiry_window(&mut self, new_window: u64) -> Result<(), String> {
if self.env().caller() != self.admin {
return Err(String::from("Only admin can set expiry window"));
}
self.expiry_window = new_window;
Ok(())
}

#[ink(message)]
pub fn create_proposal(&mut self) -> u64 {
let id = self.next_proposal_id;
self.next_proposal_id += 1;

let current_ledger = Self::current_ledger();
let expires_at = current_ledger + self.expiry_window;

let proposal = Proposal {
id,
creator: self.env().caller(),
approvals: Vec::new(),
executed: false,
expires_at,
};

self.proposals.insert(id, &proposal);
id
}

#[ink(message)]
pub fn approve_proposal(&mut self, proposal_id: u64) -> Result<(), String> {
let mut proposal = self
.proposals
.get(proposal_id)
.ok_or("Proposal not found")?;
let current_ledger = Self::current_ledger();

if current_ledger > proposal.expires_at {
return Err(String::from("Proposal expired"));
}

let caller = self.env().caller();
if !proposal.approvals.contains(&caller) {
proposal.approvals.push(caller);
}

self.proposals.insert(proposal_id, &proposal);
Ok(())
}

#[ink(message)]
pub fn finalize_proposal(&mut self, proposal_id: u64) -> Result<(), String> {
let mut proposal = self
.proposals
.get(proposal_id)
.ok_or("Proposal not found")?;
let current_ledger = Self::current_ledger();

if current_ledger > proposal.expires_at {
return Err(String::from("Proposal expired"));
}

if proposal.executed {
return Err(String::from("Already executed"));
}

// Execute logic here...
proposal.executed = true;
self.proposals.insert(proposal_id, &proposal);
Ok(())
}

#[ink(message)]
pub fn cancel_expired_proposal(&mut self, proposal_id: u64) -> Result<(), String> {
let proposal = self
.proposals
.get(proposal_id)
.ok_or("Proposal not found")?;
let current_ledger = Self::current_ledger();

if current_ledger <= proposal.expires_at {
return Err(String::from("Proposal not expired yet"));
}

self.proposals.remove(proposal_id);
Ok(())
}

fn current_ledger() -> u64 {
// Placeholder: integrate with environment ledger height
Self::env().block_number()
}
}
Loading