From 7d7c722ec98a1f55e3ff19bb799ff50c2471da85 Mon Sep 17 00:00:00 2001 From: mijinummi Date: Sun, 29 Mar 2026 14:47:39 +0100 Subject: [PATCH 1/2] Add Proposal Expiry to MultisigGovernance (#436) --- contracts/multisig_governance/src/lib.rs | 114 +++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/contracts/multisig_governance/src/lib.rs b/contracts/multisig_governance/src/lib.rs index 15ebb895..5f6abf20 100644 --- a/contracts/multisig_governance/src/lib.rs +++ b/contracts/multisig_governance/src/lib.rs @@ -558,3 +558,117 @@ 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, + pub executed: bool, + pub expires_at: u64, // NEW: ledger height expiry +} + +#[ink(storage)] +pub struct MultisigGovernance { + proposals: Mapping, + 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() + } +} From 12ad7abed892badbf5437c07e7ffbab29b8d534e Mon Sep 17 00:00:00 2001 From: mijinummi Date: Sun, 29 Mar 2026 14:54:17 +0100 Subject: [PATCH 2/2] Index LendingPool, RemittanceNFT, and MultisigGovernance Events (#437) --- backend/src/services/eventIndexer.ts | 121 +++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/backend/src/services/eventIndexer.ts b/backend/src/services/eventIndexer.ts index c99e98cb..8c8ec5e0 100644 --- a/backend/src/services/eventIndexer.ts +++ b/backend/src/services/eventIndexer.ts @@ -9,6 +9,8 @@ import { } from "./webhookService.js"; import { eventStreamService } from "./eventStreamService.js"; import { sorobanService } from "./sorobanService.js"; +import { subscribeToContractEvents } from './chainListener'; +import db from '../db'; interface SorobanRawEvent { id: string; @@ -80,6 +82,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 { if (this.running) { logger.warn("Indexer start requested while already running");