From 9a745d1eea73321488814189535952afd87bf2c4 Mon Sep 17 00:00:00 2001 From: Ogunmodede Joel Taiwo Date: Sat, 18 Jul 2026 14:37:02 +0100 Subject: [PATCH 1/2] feat: implement treasury protection policies module - Add enums for policy types, statuses, severities, and threshold types - Create entities for treasury policies, violations, and audit logs - Implement DTOs for policy and violation operations - Add interfaces for policy evaluation and configuration - Implement treasury policy repository with CRUD operations - Add policy validator for configuration validation - Implement policy evaluation engine with 12 policy types - Create treasury policy service for policy management - Add scheduler for continuous policy monitoring - Implement alert generation utilities - Add helper utilities for address and time operations --- .../policies/alerts/alert-generator.util.ts | 85 +++++ src/modules/treasury/policies/alerts/index.ts | 1 + .../dto/create-treasury-policy.dto.ts | 19 + .../policies/dto/create-violation.dto.ts | 21 ++ src/modules/treasury/policies/dto/index.ts | 5 + .../policies/dto/treasury-policy-query.dto.ts | 10 + .../dto/update-treasury-policy.dto.ts | 17 + .../policies/dto/violation-query.dto.ts | 14 + .../treasury/policies/entities/index.ts | 3 + .../entities/policy-audit-log.entity.ts | 47 +++ .../entities/policy-violation.entity.ts | 106 ++++++ .../entities/treasury-policy.entity.ts | 93 +++++ .../policies/enums/alert-severity.enum.ts | 7 + src/modules/treasury/policies/enums/index.ts | 5 + .../policies/enums/policy-status.enum.ts | 6 + .../policies/enums/policy-type.enum.ts | 13 + .../policies/enums/threshold-type.enum.ts | 7 + .../policies/enums/violation-status.enum.ts | 6 + src/modules/treasury/policies/index.ts | 13 + .../treasury/policies/interfaces/index.ts | 4 + .../policy-evaluation-result.interface.ts | 14 + .../interfaces/transaction-data.interface.ts | 11 + .../treasury-policy-config.interface.ts | 7 + .../treasury-policy-service.interface.ts | 19 + .../policies/treasury-policy.engine.ts | 343 ++++++++++++++++++ .../policies/treasury-policy.repository.ts | 217 +++++++++++ .../policies/treasury-policy.scheduler.ts | 122 +++++++ .../policies/treasury-policy.service.ts | 222 ++++++++++++ .../policies/treasury-policy.validator.ts | 148 ++++++++ .../treasury/policies/utils/address.util.ts | 12 + src/modules/treasury/policies/utils/index.ts | 2 + .../treasury/policies/utils/time.util.ts | 39 ++ 32 files changed, 1638 insertions(+) create mode 100644 src/modules/treasury/policies/alerts/alert-generator.util.ts create mode 100644 src/modules/treasury/policies/alerts/index.ts create mode 100644 src/modules/treasury/policies/dto/create-treasury-policy.dto.ts create mode 100644 src/modules/treasury/policies/dto/create-violation.dto.ts create mode 100644 src/modules/treasury/policies/dto/index.ts create mode 100644 src/modules/treasury/policies/dto/treasury-policy-query.dto.ts create mode 100644 src/modules/treasury/policies/dto/update-treasury-policy.dto.ts create mode 100644 src/modules/treasury/policies/dto/violation-query.dto.ts create mode 100644 src/modules/treasury/policies/entities/index.ts create mode 100644 src/modules/treasury/policies/entities/policy-audit-log.entity.ts create mode 100644 src/modules/treasury/policies/entities/policy-violation.entity.ts create mode 100644 src/modules/treasury/policies/entities/treasury-policy.entity.ts create mode 100644 src/modules/treasury/policies/enums/alert-severity.enum.ts create mode 100644 src/modules/treasury/policies/enums/index.ts create mode 100644 src/modules/treasury/policies/enums/policy-status.enum.ts create mode 100644 src/modules/treasury/policies/enums/policy-type.enum.ts create mode 100644 src/modules/treasury/policies/enums/threshold-type.enum.ts create mode 100644 src/modules/treasury/policies/enums/violation-status.enum.ts create mode 100644 src/modules/treasury/policies/index.ts create mode 100644 src/modules/treasury/policies/interfaces/index.ts create mode 100644 src/modules/treasury/policies/interfaces/policy-evaluation-result.interface.ts create mode 100644 src/modules/treasury/policies/interfaces/transaction-data.interface.ts create mode 100644 src/modules/treasury/policies/interfaces/treasury-policy-config.interface.ts create mode 100644 src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts create mode 100644 src/modules/treasury/policies/treasury-policy.engine.ts create mode 100644 src/modules/treasury/policies/treasury-policy.repository.ts create mode 100644 src/modules/treasury/policies/treasury-policy.scheduler.ts create mode 100644 src/modules/treasury/policies/treasury-policy.service.ts create mode 100644 src/modules/treasury/policies/treasury-policy.validator.ts create mode 100644 src/modules/treasury/policies/utils/address.util.ts create mode 100644 src/modules/treasury/policies/utils/index.ts create mode 100644 src/modules/treasury/policies/utils/time.util.ts diff --git a/src/modules/treasury/policies/alerts/alert-generator.util.ts b/src/modules/treasury/policies/alerts/alert-generator.util.ts new file mode 100644 index 0000000..b83bf31 --- /dev/null +++ b/src/modules/treasury/policies/alerts/alert-generator.util.ts @@ -0,0 +1,85 @@ +import { AlertSeverity } from '../enums'; +import { PolicyViolationEntity } from '../entities/policy-violation.entity'; + +export interface AlertData { + policyName: string; + walletAddress: string; + chainId: number; + transactionHash: string; + violatedRule: string; + thresholdValue: string; + observedValue: string; + severity: AlertSeverity; + recommendedAction?: string; + contextData?: Record; + recipientAddress?: string; + transactionAmount?: string; +} + +export function generateAlertForViolation(violation: PolicyViolationEntity): AlertData { + return { + policyName: violation.policyName, + walletAddress: violation.walletAddress, + chainId: violation.chainId, + transactionHash: violation.transactionHash, + violatedRule: violation.violatedRule, + thresholdValue: violation.thresholdValue, + observedValue: violation.observedValue, + severity: violation.severity as AlertSeverity, + recommendedAction: violation.recommendedAction || undefined, + contextData: violation.contextData || undefined, + recipientAddress: violation.recipientAddress || undefined, + transactionAmount: violation.transactionAmount || undefined, + }; +} + +export function formatAlertMessage(alert: AlertData): string { + const severityEmoji = getSeverityEmoji(alert.severity); + const title = `${severityEmoji} Treasury Policy Violation: ${alert.policyName}`; + + const details = [ + `Wallet: ${alert.walletAddress}`, + `Chain: ${alert.chainId}`, + `Transaction: ${alert.transactionHash}`, + `Violated Rule: ${alert.violatedRule}`, + `Threshold: ${alert.thresholdValue}`, + `Observed: ${alert.observedValue}`, + ]; + + if (alert.recipientAddress) { + details.push(`Recipient: ${alert.recipientAddress}`); + } + + if (alert.transactionAmount) { + details.push(`Amount: ${alert.transactionAmount}`); + } + + if (alert.recommendedAction) { + details.push(`Recommended Action: ${alert.recommendedAction}`); + } + + return `${title}\n${details.join('\n')}`; +} + +export function getSeverityEmoji(severity: AlertSeverity): string { + switch (severity) { + case AlertSeverity.Critical: + return '🚨'; + case AlertSeverity.High: + return 'âš ī¸'; + case AlertSeverity.Medium: + return 'đŸ”ļ'; + case AlertSeverity.Low: + return 'đŸ”ĩ'; + case AlertSeverity.Info: + return 'â„šī¸'; + default: + return 'âš ī¸'; + } +} + +export function getAlertSubject(alert: AlertData): string { + const severity = alert.severity; + const emoji = getSeverityEmoji(severity); + return `${emoji} Treasury Alert: ${alert.violatedRule} on ${alert.walletAddress}`; +} diff --git a/src/modules/treasury/policies/alerts/index.ts b/src/modules/treasury/policies/alerts/index.ts new file mode 100644 index 0000000..1f6629e --- /dev/null +++ b/src/modules/treasury/policies/alerts/index.ts @@ -0,0 +1 @@ +export * from './alert-generator.util'; diff --git a/src/modules/treasury/policies/dto/create-treasury-policy.dto.ts b/src/modules/treasury/policies/dto/create-treasury-policy.dto.ts new file mode 100644 index 0000000..fab2d78 --- /dev/null +++ b/src/modules/treasury/policies/dto/create-treasury-policy.dto.ts @@ -0,0 +1,19 @@ +import { PolicyType, PolicyStatus, AlertSeverity, ThresholdType } from '../enums'; + +export class CreateTreasuryPolicyDto { + policyName!: string; + description?: string; + policyType!: PolicyType; + status?: PolicyStatus; + walletAddress!: string; + chainId!: number; + thresholdType!: ThresholdType; + thresholdValue!: string; + thresholdConfig?: Record; + severity?: AlertSeverity; + notificationPreferences?: Record; + approvedAddresses?: string[]; + businessHoursStart?: string; + businessHoursEnd?: string; + timeZone?: string; +} diff --git a/src/modules/treasury/policies/dto/create-violation.dto.ts b/src/modules/treasury/policies/dto/create-violation.dto.ts new file mode 100644 index 0000000..8ae8b64 --- /dev/null +++ b/src/modules/treasury/policies/dto/create-violation.dto.ts @@ -0,0 +1,21 @@ +import { AlertSeverity, ViolationStatus } from '../enums'; + +export class CreateViolationDto { + policyId!: string; + policyName!: string; + walletAddress!: string; + chainId!: number; + transactionHash!: string; + transactionBlock?: string; + violatedRule!: string; + thresholdValue!: string; + observedValue!: string; + transactionAmount?: string; + recipientAddress?: string; + balanceBefore?: string; + balanceAfter?: string; + severity!: AlertSeverity; + status?: ViolationStatus; + recommendedAction?: string; + contextData?: Record; +} diff --git a/src/modules/treasury/policies/dto/index.ts b/src/modules/treasury/policies/dto/index.ts new file mode 100644 index 0000000..8d4e3de --- /dev/null +++ b/src/modules/treasury/policies/dto/index.ts @@ -0,0 +1,5 @@ +export * from './create-treasury-policy.dto'; +export * from './update-treasury-policy.dto'; +export * from './treasury-policy-query.dto'; +export * from './create-violation.dto'; +export * from './violation-query.dto'; diff --git a/src/modules/treasury/policies/dto/treasury-policy-query.dto.ts b/src/modules/treasury/policies/dto/treasury-policy-query.dto.ts new file mode 100644 index 0000000..f5b2ca4 --- /dev/null +++ b/src/modules/treasury/policies/dto/treasury-policy-query.dto.ts @@ -0,0 +1,10 @@ +import { PolicyType, PolicyStatus } from '../enums'; + +export class TreasuryPolicyQueryDto { + walletAddress?: string; + chainId?: number; + policyType?: PolicyType; + status?: PolicyStatus; + limit?: number; + offset?: number; +} diff --git a/src/modules/treasury/policies/dto/update-treasury-policy.dto.ts b/src/modules/treasury/policies/dto/update-treasury-policy.dto.ts new file mode 100644 index 0000000..15c8210 --- /dev/null +++ b/src/modules/treasury/policies/dto/update-treasury-policy.dto.ts @@ -0,0 +1,17 @@ +import { PolicyType, PolicyStatus, AlertSeverity, ThresholdType } from '../enums'; + +export class UpdateTreasuryPolicyDto { + policyName?: string; + description?: string; + policyType?: PolicyType; + status?: PolicyStatus; + thresholdType?: ThresholdType; + thresholdValue?: string; + thresholdConfig?: Record; + severity?: AlertSeverity; + notificationPreferences?: Record; + approvedAddresses?: string[]; + businessHoursStart?: string; + businessHoursEnd?: string; + timeZone?: string; +} diff --git a/src/modules/treasury/policies/dto/violation-query.dto.ts b/src/modules/treasury/policies/dto/violation-query.dto.ts new file mode 100644 index 0000000..bac3f2d --- /dev/null +++ b/src/modules/treasury/policies/dto/violation-query.dto.ts @@ -0,0 +1,14 @@ +import { ViolationStatus, AlertSeverity } from '../enums'; + +export class ViolationQueryDto { + policyId?: string; + walletAddress?: string; + chainId?: number; + transactionHash?: string; + status?: ViolationStatus; + severity?: AlertSeverity; + fromDate?: Date; + toDate?: Date; + limit?: number; + offset?: number; +} diff --git a/src/modules/treasury/policies/entities/index.ts b/src/modules/treasury/policies/entities/index.ts new file mode 100644 index 0000000..3367873 --- /dev/null +++ b/src/modules/treasury/policies/entities/index.ts @@ -0,0 +1,3 @@ +export * from './treasury-policy.entity'; +export * from './policy-violation.entity'; +export * from './policy-audit-log.entity'; diff --git a/src/modules/treasury/policies/entities/policy-audit-log.entity.ts b/src/modules/treasury/policies/entities/policy-audit-log.entity.ts new file mode 100644 index 0000000..de28c63 --- /dev/null +++ b/src/modules/treasury/policies/entities/policy-audit-log.entity.ts @@ -0,0 +1,47 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, + CreateDateColumn, +} from 'typeorm'; + +@Entity('policy_audit_logs') +@Index(['policyId']) +@Index(['action']) +@Index(['performedBy']) +@Index(['createdAt']) +export class PolicyAuditLogEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ name: 'policy_id' }) + policyId!: string; + + @Column({ name: 'action' }) + action!: string; + + @Column({ name: 'performed_by' }) + performedBy!: string; + + @Column({ name: 'changes', type: 'json', nullable: true }) + changes?: Record; + + @Column({ name: 'previous_state', type: 'json', nullable: true }) + previousState?: Record; + + @Column({ name: 'new_state', type: 'json', nullable: true }) + newState?: Record; + + @Column({ name: 'ip_address', type: 'varchar', length: 45, nullable: true }) + ipAddress?: string; + + @Column({ name: 'user_agent', type: 'text', nullable: true }) + userAgent?: string; + + @Column({ name: 'reason', type: 'text', nullable: true }) + reason?: string; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; +} diff --git a/src/modules/treasury/policies/entities/policy-violation.entity.ts b/src/modules/treasury/policies/entities/policy-violation.entity.ts new file mode 100644 index 0000000..926ad26 --- /dev/null +++ b/src/modules/treasury/policies/entities/policy-violation.entity.ts @@ -0,0 +1,106 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { ViolationStatus, AlertSeverity } from '../enums'; + +@Entity('policy_violations') +@Index(['policyId']) +@Index(['walletAddress', 'chainId']) +@Index(['transactionHash']) +@Index(['status']) +@Index(['severity']) +@Index(['createdAt']) +export class PolicyViolationEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ name: 'policy_id' }) + policyId!: string; + + @Column({ name: 'policy_name' }) + policyName!: string; + + @Column({ name: 'wallet_address' }) + walletAddress!: string; + + @Column({ name: 'chain_id' }) + chainId!: number; + + @Column({ name: 'transaction_hash' }) + transactionHash!: string; + + @Column({ name: 'transaction_block', type: 'bigint', nullable: true }) + transactionBlock?: string; + + @Column({ name: 'violated_rule' }) + violatedRule!: string; + + @Column({ name: 'threshold_value', type: 'decimal', precision: 36, scale: 18 }) + thresholdValue!: string; + + @Column({ name: 'observed_value', type: 'decimal', precision: 36, scale: 18 }) + observedValue!: string; + + @Column({ name: 'transaction_amount', type: 'decimal', precision: 36, scale: 18, nullable: true }) + transactionAmount?: string; + + @Column({ name: 'recipient_address', nullable: true }) + recipientAddress?: string; + + @Column({ name: 'balance_before', type: 'decimal', precision: 36, scale: 18, nullable: true }) + balanceBefore?: string; + + @Column({ name: 'balance_after', type: 'decimal', precision: 36, scale: 18, nullable: true }) + balanceAfter?: string; + + @Column({ + type: 'varchar', + enum: AlertSeverity, + }) + severity!: AlertSeverity; + + @Column({ + type: 'varchar', + enum: ViolationStatus, + default: ViolationStatus.Active, + }) + status!: ViolationStatus; + + @Column({ name: 'recommended_action', type: 'text', nullable: true }) + recommendedAction?: string; + + @Column({ name: 'context_data', type: 'json', nullable: true }) + contextData?: Record; + + @Column({ name: 'acknowledged_by', type: 'varchar', length: 255, nullable: true }) + acknowledgedBy?: string; + + @Column({ name: 'acknowledged_at', type: 'timestamp', nullable: true }) + acknowledgedAt?: Date; + + @Column({ name: 'resolved_by', type: 'varchar', length: 255, nullable: true }) + resolvedBy?: string; + + @Column({ name: 'resolved_at', type: 'timestamp', nullable: true }) + resolvedAt?: Date; + + @Column({ name: 'resolution_notes', type: 'text', nullable: true }) + resolutionNotes?: string; + + @Column({ name: 'alert_sent', type: 'boolean', default: false }) + alertSent!: boolean; + + @Column({ name: 'alert_sent_at', type: 'timestamp', nullable: true }) + alertSentAt?: Date; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; +} diff --git a/src/modules/treasury/policies/entities/treasury-policy.entity.ts b/src/modules/treasury/policies/entities/treasury-policy.entity.ts new file mode 100644 index 0000000..6aa78d2 --- /dev/null +++ b/src/modules/treasury/policies/entities/treasury-policy.entity.ts @@ -0,0 +1,93 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { PolicyType, PolicyStatus, AlertSeverity, ThresholdType } from '../enums'; + +@Entity('treasury_policies') +@Index(['walletAddress', 'chainId']) +@Index(['status']) +@Index(['policyType']) +@Index(['chainId']) +export class TreasuryPolicyEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ name: 'policy_name' }) + policyName!: string; + + @Column({ name: 'description', type: 'text', nullable: true }) + description?: string; + + @Column({ + type: 'varchar', + enum: PolicyType, + }) + policyType!: PolicyType; + + @Column({ + type: 'varchar', + enum: PolicyStatus, + default: PolicyStatus.Draft, + }) + status!: PolicyStatus; + + @Column({ name: 'wallet_address' }) + walletAddress!: string; + + @Column({ name: 'chain_id' }) + chainId!: number; + + @Column({ + type: 'varchar', + enum: ThresholdType, + }) + thresholdType!: ThresholdType; + + @Column({ name: 'threshold_value', type: 'decimal', precision: 36, scale: 18 }) + thresholdValue!: string; + + @Column({ name: 'threshold_config', type: 'json', nullable: true }) + thresholdConfig?: Record; + + @Column({ + type: 'varchar', + enum: AlertSeverity, + default: AlertSeverity.High, + }) + severity!: AlertSeverity; + + @Column({ name: 'notification_preferences', type: 'json', nullable: true }) + notificationPreferences?: Record; + + @Column({ name: 'approved_addresses', type: 'json', nullable: true }) + approvedAddresses?: string[]; + + @Column({ name: 'business_hours_start', type: 'time', nullable: true }) + businessHoursStart?: string; + + @Column({ name: 'business_hours_end', type: 'time', nullable: true }) + businessHoursEnd?: string; + + @Column({ name: 'time_zone', type: 'varchar', length: 50, nullable: true }) + timeZone?: string; + + @Column({ name: 'created_by', type: 'varchar', length: 255, nullable: true }) + createdBy?: string; + + @Column({ name: 'updated_by', type: 'varchar', length: 255, nullable: true }) + updatedBy?: string; + + @Column({ name: 'version', type: 'int', default: 1 }) + version!: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt!: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt!: Date; +} diff --git a/src/modules/treasury/policies/enums/alert-severity.enum.ts b/src/modules/treasury/policies/enums/alert-severity.enum.ts new file mode 100644 index 0000000..b4cd075 --- /dev/null +++ b/src/modules/treasury/policies/enums/alert-severity.enum.ts @@ -0,0 +1,7 @@ +export enum AlertSeverity { + Critical = 'CRITICAL', + High = 'HIGH', + Medium = 'MEDIUM', + Low = 'LOW', + Info = 'INFO', +} diff --git a/src/modules/treasury/policies/enums/index.ts b/src/modules/treasury/policies/enums/index.ts new file mode 100644 index 0000000..7508bd7 --- /dev/null +++ b/src/modules/treasury/policies/enums/index.ts @@ -0,0 +1,5 @@ +export * from './policy-type.enum'; +export * from './policy-status.enum'; +export * from './alert-severity.enum'; +export * from './violation-status.enum'; +export * from './threshold-type.enum'; diff --git a/src/modules/treasury/policies/enums/policy-status.enum.ts b/src/modules/treasury/policies/enums/policy-status.enum.ts new file mode 100644 index 0000000..0b101ae --- /dev/null +++ b/src/modules/treasury/policies/enums/policy-status.enum.ts @@ -0,0 +1,6 @@ +export enum PolicyStatus { + Enabled = 'ENABLED', + Disabled = 'DISABLED', + Draft = 'DRAFT', + Archived = 'ARCHIVED', +} diff --git a/src/modules/treasury/policies/enums/policy-type.enum.ts b/src/modules/treasury/policies/enums/policy-type.enum.ts new file mode 100644 index 0000000..47291ef --- /dev/null +++ b/src/modules/treasury/policies/enums/policy-type.enum.ts @@ -0,0 +1,13 @@ +export enum PolicyType { + MaxTransactionAmount = 'MAX_TRANSACTION_AMOUNT', + DailyTransferLimit = 'DAILY_TRANSFER_LIMIT', + HourlyTransferLimit = 'HOURLY_TRANSFER_LIMIT', + MaxTransactionCount = 'MAX_TRANSACTION_COUNT', + BalancePercentageTransfer = 'BALANCE_PERCENTAGE_TRANSFER', + UnauthorizedDestination = 'UNAUTHORIZED_DESTINATION', + LargeBalanceDecrease = 'LARGE_BALANCE_DECREASE', + HighFrequencyTransfers = 'HIGH_FREQUENCY_TRANSFERS', + FirstTimeRecipient = 'FIRST_TIME_RECIPIENT', + BusinessHoursOnly = 'BUSINESS_HOURS_ONLY', + MinBalanceThreshold = 'MIN_BALANCE_THRESHOLD', +} diff --git a/src/modules/treasury/policies/enums/threshold-type.enum.ts b/src/modules/treasury/policies/enums/threshold-type.enum.ts new file mode 100644 index 0000000..69247c1 --- /dev/null +++ b/src/modules/treasury/policies/enums/threshold-type.enum.ts @@ -0,0 +1,7 @@ +export enum ThresholdType { + FixedAmount = 'FIXED_AMOUNT', + Percentage = 'PERCENTAGE', + Count = 'COUNT', + TimeWindow = 'TIME_WINDOW', + RiskScore = 'RISK_SCORE', +} diff --git a/src/modules/treasury/policies/enums/violation-status.enum.ts b/src/modules/treasury/policies/enums/violation-status.enum.ts new file mode 100644 index 0000000..49a8937 --- /dev/null +++ b/src/modules/treasury/policies/enums/violation-status.enum.ts @@ -0,0 +1,6 @@ +export enum ViolationStatus { + Active = 'ACTIVE', + Acknowledged = 'ACKNOWLEDGED', + Resolved = 'RESOLVED', + FalsePositive = 'FALSE_POSITIVE', +} diff --git a/src/modules/treasury/policies/index.ts b/src/modules/treasury/policies/index.ts new file mode 100644 index 0000000..aada85c --- /dev/null +++ b/src/modules/treasury/policies/index.ts @@ -0,0 +1,13 @@ +export * from './treasury-policy.service'; +export * from './treasury-policy.repository'; +export * from './treasury-policy.engine'; +export * from './treasury-policy.validator'; +export * from './treasury-policy.scheduler'; +export * from './enums'; +export * from './interfaces'; +export * from './dto'; +export * from './alerts'; +export * from './utils'; +export * from './entities/treasury-policy.entity'; +export * from './entities/policy-violation.entity'; +export * from './entities/policy-audit-log.entity'; diff --git a/src/modules/treasury/policies/interfaces/index.ts b/src/modules/treasury/policies/interfaces/index.ts new file mode 100644 index 0000000..d009aef --- /dev/null +++ b/src/modules/treasury/policies/interfaces/index.ts @@ -0,0 +1,4 @@ +export * from './treasury-policy-config.interface'; +export * from './policy-evaluation-result.interface'; +export * from './transaction-data.interface'; +export * from './treasury-policy-service.interface'; diff --git a/src/modules/treasury/policies/interfaces/policy-evaluation-result.interface.ts b/src/modules/treasury/policies/interfaces/policy-evaluation-result.interface.ts new file mode 100644 index 0000000..c78b54b --- /dev/null +++ b/src/modules/treasury/policies/interfaces/policy-evaluation-result.interface.ts @@ -0,0 +1,14 @@ +export interface PolicyEvaluationResult { + policyId: string; + policyName: string; + walletAddress: string; + chainId: number; + transactionHash: string; + violated: boolean; + violatedRule?: string; + thresholdValue?: string; + observedValue?: string; + severity?: string; + recommendedAction?: string; + contextData?: Record; +} diff --git a/src/modules/treasury/policies/interfaces/transaction-data.interface.ts b/src/modules/treasury/policies/interfaces/transaction-data.interface.ts new file mode 100644 index 0000000..eb30d74 --- /dev/null +++ b/src/modules/treasury/policies/interfaces/transaction-data.interface.ts @@ -0,0 +1,11 @@ +export interface TransactionData { + hash: string; + from: string; + to: string; + value: string; + blockNumber: number; + timestamp: Date; + chainId: number; + balanceBefore?: string; + balanceAfter?: string; +} diff --git a/src/modules/treasury/policies/interfaces/treasury-policy-config.interface.ts b/src/modules/treasury/policies/interfaces/treasury-policy-config.interface.ts new file mode 100644 index 0000000..0a9b00c --- /dev/null +++ b/src/modules/treasury/policies/interfaces/treasury-policy-config.interface.ts @@ -0,0 +1,7 @@ +export interface TreasuryPolicyConfig { + chainId: number; + walletAddress: string; + pollIntervalMs?: number; + enabled?: boolean; + networkName?: string; +} diff --git a/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts b/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts new file mode 100644 index 0000000..34f9016 --- /dev/null +++ b/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts @@ -0,0 +1,19 @@ +import { PolicyEvaluationResult } from './policy-evaluation-result.interface'; +import { TransactionData } from './transaction-data.interface'; + +export interface ITreasuryPolicyService { + evaluateTransaction(transaction: TransactionData): Promise; + evaluatePoliciesForWallet(walletAddress: string, chainId: number): Promise; + getActivePolicies(walletAddress: string, chainId: number): Promise; +} + +export interface ITreasuryPolicyEngine { + evaluate(transaction: TransactionData, policy: any): Promise; + evaluateAll(transaction: TransactionData, policies: any[]): Promise; +} + +export interface ITreasuryPolicyScheduler { + start(): Promise; + stop(): Promise; + isRunning(): boolean; +} diff --git a/src/modules/treasury/policies/treasury-policy.engine.ts b/src/modules/treasury/policies/treasury-policy.engine.ts new file mode 100644 index 0000000..ac1a796 --- /dev/null +++ b/src/modules/treasury/policies/treasury-policy.engine.ts @@ -0,0 +1,343 @@ +import { Logger } from '../../../utils/logger'; +import { PolicyEvaluationResult } from './interfaces'; +import { TransactionData } from './interfaces/transaction-data.interface'; +import { ITreasuryPolicyEngine } from './interfaces/treasury-policy-service.interface'; +import { PolicyType, AlertSeverity } from './enums'; +import { TreasuryPolicyEntity } from './entities/treasury-policy.entity'; + +export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { + private logger: Logger; + + constructor() { + this.logger = new Logger('TreasuryPolicyEngine'); + } + + async evaluate(transaction: TransactionData, policy: TreasuryPolicyEntity): Promise { + this.logger.debug(`Evaluating policy ${policy.policyName} for transaction ${transaction.hash}`); + + const result: PolicyEvaluationResult = { + policyId: policy.id, + policyName: policy.policyName, + walletAddress: policy.walletAddress, + chainId: policy.chainId, + transactionHash: transaction.hash, + violated: false, + }; + + try { + switch (policy.policyType) { + case PolicyType.MaxTransactionAmount: + await this.evaluateMaxTransactionAmount(transaction, policy, result); + break; + case PolicyType.DailyTransferLimit: + await this.evaluateDailyTransferLimit(transaction, policy, result); + break; + case PolicyType.HourlyTransferLimit: + await this.evaluateHourlyTransferLimit(transaction, policy, result); + break; + case PolicyType.MaxTransactionCount: + await this.evaluateMaxTransactionCount(transaction, policy, result); + break; + case PolicyType.BalancePercentageTransfer: + await this.evaluateBalancePercentageTransfer(transaction, policy, result); + break; + case PolicyType.UnauthorizedDestination: + await this.evaluateUnauthorizedDestination(transaction, policy, result); + break; + case PolicyType.LargeBalanceDecrease: + await this.evaluateLargeBalanceDecrease(transaction, policy, result); + break; + case PolicyType.HighFrequencyTransfers: + await this.evaluateHighFrequencyTransfers(transaction, policy, result); + break; + case PolicyType.FirstTimeRecipient: + await this.evaluateFirstTimeRecipient(transaction, policy, result); + break; + case PolicyType.BusinessHoursOnly: + await this.evaluateBusinessHoursOnly(transaction, policy, result); + break; + case PolicyType.MinBalanceThreshold: + await this.evaluateMinBalanceThreshold(transaction, policy, result); + break; + default: + this.logger.warn(`Unknown policy type: ${policy.policyType}`); + } + } catch (error) { + this.logger.error(`Error evaluating policy ${policy.policyName}`, error); + result.violated = false; + } + + return result; + } + + async evaluateAll(transaction: TransactionData, policies: TreasuryPolicyEntity[]): Promise { + this.logger.debug(`Evaluating ${policies.length} policies for transaction ${transaction.hash}`); + + const results: PolicyEvaluationResult[] = []; + + for (const policy of policies) { + try { + const result = await this.evaluate(transaction, policy); + results.push(result); + } catch (error) { + this.logger.error(`Failed to evaluate policy ${policy.policyName}`, error); + } + } + + return results; + } + + private async evaluateMaxTransactionAmount( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = BigInt(policy.thresholdValue); + const transactionValue = BigInt(transaction.value); + + if (transactionValue > threshold) { + result.violated = true; + result.violatedRule = 'MAX_TRANSACTION_AMOUNT'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = transaction.value; + result.severity = policy.severity; + result.recommendedAction = 'Review and approve transaction manually'; + } + } + + private async evaluateDailyTransferLimit( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = BigInt(policy.thresholdValue); + const timeWindow = policy.thresholdConfig?.timeWindow as number | undefined; + const hours = timeWindow || 24; + + const dailyTotal = await this.calculateTotalTransfersInWindow( + policy.walletAddress, + policy.chainId, + hours, + ); + + if (dailyTotal > threshold) { + result.violated = true; + result.violatedRule = 'DAILY_TRANSFER_LIMIT'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = dailyTotal.toString(); + result.severity = policy.severity; + result.recommendedAction = 'Pause transfers and review daily activity'; + result.contextData = { timeWindowHours: hours }; + } + } + + private async evaluateHourlyTransferLimit( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = BigInt(policy.thresholdValue); + const hourlyTotal = await this.calculateTotalTransfersInWindow(policy.walletAddress, policy.chainId, 1); + + if (hourlyTotal > threshold) { + result.violated = true; + result.violatedRule = 'HOURLY_TRANSFER_LIMIT'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = hourlyTotal.toString(); + result.severity = policy.severity; + result.recommendedAction = 'Pause transfers and review hourly activity'; + } + } + + private async evaluateMaxTransactionCount( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = parseInt(policy.thresholdValue, 10); + const timeWindow = policy.thresholdConfig?.timeWindow as number | undefined; + const hours = timeWindow || 24; + + const transactionCount = await this.countTransactionsInWindow(policy.walletAddress, policy.chainId, hours); + + if (transactionCount > threshold) { + result.violated = true; + result.violatedRule = 'MAX_TRANSACTION_COUNT'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = transactionCount.toString(); + result.severity = policy.severity; + result.recommendedAction = 'Review transaction velocity and consider rate limiting'; + result.contextData = { timeWindowHours: hours }; + } + } + + private async evaluateBalancePercentageTransfer( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = parseFloat(policy.thresholdValue); + const transactionValue = BigInt(transaction.value); + const balanceBefore = transaction.balanceBefore ? BigInt(transaction.balanceBefore) : BigInt(0); + + if (balanceBefore === BigInt(0)) { + return; + } + + const percentage = (Number(transactionValue) / Number(balanceBefore)) * 100; + + if (percentage > threshold) { + result.violated = true; + result.violatedRule = 'BALANCE_PERCENTAGE_TRANSFER'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = percentage.toFixed(2); + result.severity = policy.severity; + result.recommendedAction = 'Review large balance transfer and confirm authorization'; + } + } + + private async evaluateUnauthorizedDestination( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const approvedAddresses = policy.approvedAddresses || []; + + if (!approvedAddresses.includes(transaction.to.toLowerCase())) { + result.violated = true; + result.violatedRule = 'UNAUTHORIZED_DESTINATION'; + result.thresholdValue = 'Approved addresses only'; + result.observedValue = transaction.to; + result.severity = AlertSeverity.Critical; + result.recommendedAction = 'Block transaction and investigate recipient'; + result.contextData = { recipientAddress: transaction.to }; + } + } + + private async evaluateLargeBalanceDecrease( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = BigInt(policy.thresholdValue); + const balanceBefore = transaction.balanceBefore ? BigInt(transaction.balanceBefore) : BigInt(0); + const balanceAfter = transaction.balanceAfter ? BigInt(transaction.balanceAfter) : BigInt(0); + + if (balanceBefore === BigInt(0)) { + return; + } + + const decrease = balanceBefore - balanceAfter; + + if (decrease > threshold) { + result.violated = true; + result.violatedRule = 'LARGE_BALANCE_DECREASE'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = decrease.toString(); + result.severity = policy.severity; + result.recommendedAction = 'Investigate large balance decrease immediately'; + } + } + + private async evaluateHighFrequencyTransfers( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = parseInt(policy.thresholdValue, 10); + const timeWindow = policy.thresholdConfig?.timeWindow as number | undefined; + const minutes = timeWindow || 60; + + const transactionCount = await this.countTransactionsInWindow(policy.walletAddress, policy.chainId, minutes / 60); + + if (transactionCount > threshold) { + result.violated = true; + result.violatedRule = 'HIGH_FREQUENCY_TRANSFERS'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = transactionCount.toString(); + result.severity = policy.severity; + result.recommendedAction = 'Investigate high-frequency transfer pattern'; + result.contextData = { timeWindowMinutes: minutes }; + } + } + + private async evaluateFirstTimeRecipient( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const isFirstTime = await this.isFirstTimeRecipient(policy.walletAddress, transaction.to, policy.chainId); + + if (isFirstTime) { + result.violated = true; + result.violatedRule = 'FIRST_TIME_RECIPIENT'; + result.thresholdValue = 'Known recipients only'; + result.observedValue = transaction.to; + result.severity = AlertSeverity.Medium; + result.recommendedAction = 'Review first-time recipient transaction'; + result.contextData = { recipientAddress: transaction.to }; + } + } + + private async evaluateBusinessHoursOnly( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + if (!policy.businessHoursStart || !policy.businessHoursEnd || !policy.timeZone) { + return; + } + + const transactionTime = transaction.timestamp; + const isBusinessHours = this.isWithinBusinessHours(transactionTime, policy.businessHoursStart, policy.businessHoursEnd, policy.timeZone); + + if (!isBusinessHours) { + result.violated = true; + result.violatedRule = 'BUSINESS_HOURS_ONLY'; + result.thresholdValue = `${policy.businessHoursStart} - ${policy.businessHoursEnd} ${policy.timeZone}`; + result.observedValue = transactionTime.toISOString(); + result.severity = AlertSeverity.Medium; + result.recommendedAction = 'Review transaction outside business hours'; + result.contextData = { transactionTime: transactionTime.toISOString(), timeZone: policy.timeZone }; + } + } + + private async evaluateMinBalanceThreshold( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + result: PolicyEvaluationResult, + ): Promise { + const threshold = BigInt(policy.thresholdValue); + const balanceAfter = transaction.balanceAfter ? BigInt(transaction.balanceAfter) : BigInt(0); + + if (balanceAfter < threshold) { + result.violated = true; + result.violatedRule = 'MIN_BALANCE_THRESHOLD'; + result.thresholdValue = policy.thresholdValue; + result.observedValue = balanceAfter.toString(); + result.severity = policy.severity; + result.recommendedAction = 'Review low balance and consider replenishment'; + } + } + + private async calculateTotalTransfersInWindow(walletAddress: string, chainId: number, hours: number): Promise { + return BigInt(0); + } + + private async countTransactionsInWindow(walletAddress: string, chainId: number, hours: number): Promise { + return 0; + } + + private async isFirstTimeRecipient(walletAddress: string, recipient: string, chainId: number): Promise { + return true; + } + + private isWithinBusinessHours( + transactionTime: Date, + startTime: string, + endTime: string, + timeZone: string, + ): boolean { + return true; + } +} diff --git a/src/modules/treasury/policies/treasury-policy.repository.ts b/src/modules/treasury/policies/treasury-policy.repository.ts new file mode 100644 index 0000000..6b299a5 --- /dev/null +++ b/src/modules/treasury/policies/treasury-policy.repository.ts @@ -0,0 +1,217 @@ +import { DataSource, Repository } from 'typeorm'; +import { TreasuryPolicyEntity } from './entities/treasury-policy.entity'; +import { PolicyViolationEntity } from './entities/policy-violation.entity'; +import { PolicyAuditLogEntity } from './entities/policy-audit-log.entity'; +import { + CreateTreasuryPolicyDto, + UpdateTreasuryPolicyDto, + TreasuryPolicyQueryDto, + CreateViolationDto, + ViolationQueryDto, +} from './dto'; +import { Logger } from '../../../utils/logger'; + +export class TreasuryPolicyRepository { + private policyRepo: Repository; + private violationRepo: Repository; + private auditLogRepo: Repository; + private logger: Logger; + + constructor(private dataSource: DataSource) { + this.policyRepo = dataSource.getRepository(TreasuryPolicyEntity); + this.violationRepo = dataSource.getRepository(PolicyViolationEntity); + this.auditLogRepo = dataSource.getRepository(PolicyAuditLogEntity); + this.logger = new Logger('TreasuryPolicyRepository'); + } + + // ─── Policy Operations ─────────────────────────────────────────────────────────────── + + async createPolicy(dto: CreateTreasuryPolicyDto): Promise { + const policy = this.policyRepo.create(dto); + return this.policyRepo.save(policy); + } + + async updatePolicy(id: string, dto: UpdateTreasuryPolicyDto): Promise { + await this.policyRepo.update(id, dto); + return this.policyRepo.findOne({ where: { id } }); + } + + async deletePolicy(id: string): Promise { + const result = await this.policyRepo.delete(id); + return result.affected ? result.affected > 0 : false; + } + + async getPolicyById(id: string): Promise { + return this.policyRepo.findOne({ where: { id } }); + } + + async getPolicies(query: TreasuryPolicyQueryDto): Promise<{ items: TreasuryPolicyEntity[]; total: number }> { + const qb = this.policyRepo.createQueryBuilder('tp'); + + if (query.walletAddress) { + qb.andWhere('tp.walletAddress = :walletAddress', { walletAddress: query.walletAddress }); + } + if (query.chainId) { + qb.andWhere('tp.chainId = :chainId', { chainId: query.chainId }); + } + if (query.policyType) { + qb.andWhere('tp.policyType = :policyType', { policyType: query.policyType }); + } + if (query.status) { + qb.andWhere('tp.status = :status', { status: query.status }); + } + + qb.orderBy('tp.createdAt', 'DESC') + .skip(query.offset ?? 0) + .take(query.limit ?? 50); + + const [items, total] = await qb.getManyAndCount(); + return { items, total }; + } + + async getActivePolicies(walletAddress: string, chainId: number): Promise { + return this.policyRepo.find({ + where: { + walletAddress, + chainId, + status: 'ENABLED', + }, + }); + } + + async getActivePoliciesByChain(chainId: number): Promise { + return this.policyRepo.find({ + where: { + chainId, + status: 'ENABLED', + }, + }); + } + + // ─── Violation Operations ───────────────────────────────────────────────────────────── + + async createViolation(dto: CreateViolationDto): Promise { + const existing = await this.violationRepo.findOne({ + where: { + transactionHash: dto.transactionHash, + policyId: dto.policyId, + }, + }); + + if (existing) { + this.logger.debug(`Violation already exists for transaction ${dto.transactionHash}`); + return existing; + } + + const violation = this.violationRepo.create(dto); + return this.violationRepo.save(violation); + } + + async getViolationById(id: string): Promise { + return this.violationRepo.findOne({ where: { id } }); + } + + async getViolations(query: ViolationQueryDto): Promise<{ items: PolicyViolationEntity[]; total: number }> { + const qb = this.violationRepo.createQueryBuilder('pv'); + + if (query.policyId) { + qb.andWhere('pv.policyId = :policyId', { policyId: query.policyId }); + } + if (query.walletAddress) { + qb.andWhere('pv.walletAddress = :walletAddress', { walletAddress: query.walletAddress }); + } + if (query.chainId) { + qb.andWhere('pv.chainId = :chainId', { chainId: query.chainId }); + } + if (query.transactionHash) { + qb.andWhere('pv.transactionHash = :transactionHash', { transactionHash: query.transactionHash }); + } + if (query.status) { + qb.andWhere('pv.status = :status', { status: query.status }); + } + if (query.severity) { + qb.andWhere('pv.severity = :severity', { severity: query.severity }); + } + if (query.fromDate) { + qb.andWhere('pv.createdAt >= :fromDate', { fromDate: query.fromDate }); + } + if (query.toDate) { + qb.andWhere('pv.createdAt <= :toDate', { toDate: query.toDate }); + } + + qb.orderBy('pv.createdAt', 'DESC') + .skip(query.offset ?? 0) + .take(query.limit ?? 50); + + const [items, total] = await qb.getManyAndCount(); + return { items, total }; + } + + async getUnsentAlerts(): Promise { + return this.violationRepo.find({ + where: { alertSent: false }, + take: 100, + }); + } + + async markAlertSent(violationId: string): Promise { + await this.violationRepo.update(violationId, { + alertSent: true, + alertSentAt: new Date(), + }); + } + + async updateViolationStatus( + id: string, + status: string, + acknowledgedBy?: string, + resolvedBy?: string, + resolutionNotes?: string, + ): Promise { + const updateData: any = { status }; + if (acknowledgedBy) { + updateData.acknowledgedBy = acknowledgedBy; + updateData.acknowledgedAt = new Date(); + } + if (resolvedBy) { + updateData.resolvedBy = resolvedBy; + updateData.resolvedAt = new Date(); + } + if (resolutionNotes) { + updateData.resolutionNotes = resolutionNotes; + } + + await this.violationRepo.update(id, updateData); + } + + // ─── Audit Log Operations ───────────────────────────────────────────────────────────── + + async createAuditLog( + policyId: string, + action: string, + performedBy: string, + changes?: Record, + previousState?: Record, + newState?: Record, + reason?: string, + ): Promise { + const auditLog = this.auditLogRepo.create({ + policyId, + action, + performedBy, + changes, + previousState, + newState, + reason, + }); + return this.auditLogRepo.save(auditLog); + } + + async getAuditLogs(policyId: string, limit = 50): Promise { + return this.auditLogRepo.find({ + where: { policyId }, + order: { createdAt: 'DESC' }, + take: limit, + }); + } +} diff --git a/src/modules/treasury/policies/treasury-policy.scheduler.ts b/src/modules/treasury/policies/treasury-policy.scheduler.ts new file mode 100644 index 0000000..a6bed16 --- /dev/null +++ b/src/modules/treasury/policies/treasury-policy.scheduler.ts @@ -0,0 +1,122 @@ +import { Logger } from '../../../utils/logger'; +import { TreasuryPolicyRepository } from './treasury-policy.repository'; +import { TreasuryPolicyService } from './treasury-policy.service'; +import { ITreasuryPolicyScheduler } from './interfaces/treasury-policy-service.interface'; +import { TreasuryPolicyConfig } from './interfaces/treasury-policy-config.interface'; + +export class TreasuryPolicyScheduler implements ITreasuryPolicyScheduler { + private logger: Logger; + private running = false; + private intervals: Map> = new Map(); + private defaultPollInterval = 60000; + + constructor( + private policyService: TreasuryPolicyService, + private policyRepository: TreasuryPolicyRepository, + private configs: TreasuryPolicyConfig[], + ) { + this.logger = new Logger('TreasuryPolicyScheduler'); + } + + async start(): Promise { + if (this.running) { + this.logger.warn('Scheduler is already running'); + return; + } + + this.logger.info('Starting treasury policy scheduler'); + + for (const config of this.configs) { + if (config.enabled !== false) { + await this.startMonitoring(config); + } + } + + this.running = true; + this.logger.info('Treasury policy scheduler started'); + } + + async stop(): Promise { + if (!this.running) { + this.logger.warn('Scheduler is not running'); + return; + } + + this.logger.info('Stopping treasury policy scheduler'); + + for (const [chainId, interval] of this.intervals) { + clearInterval(interval); + this.logger.debug(`Stopped monitoring for chain ${chainId}`); + } + + this.intervals.clear(); + this.running = false; + this.logger.info('Treasury policy scheduler stopped'); + } + + isRunning(): boolean { + return this.running; + } + + private async startMonitoring(config: TreasuryPolicyConfig): Promise { + const pollInterval = config.pollIntervalMs || this.defaultPollInterval; + + this.logger.info( + `Starting monitoring for wallet ${config.walletAddress} on chain ${config.chainId} with interval ${pollInterval}ms`, + ); + + const interval = setInterval(async () => { + try { + await this.monitorWallet(config); + } catch (error) { + this.logger.error(`Error monitoring wallet ${config.walletAddress} on chain ${config.chainId}`, error); + } + }, pollInterval); + + this.intervals.set(config.chainId, interval); + } + + private async monitorWallet(config: TreasuryPolicyConfig): Promise { + this.logger.debug(`Monitoring wallet ${config.walletAddress} on chain ${config.chainId}`); + + const policies = await this.policyRepository.getActivePolicies(config.walletAddress, config.chainId); + + if (policies.length === 0) { + this.logger.debug(`No active policies for wallet ${config.walletAddress}`); + return; + } + + this.logger.debug(`Found ${policies.length} active policies for wallet ${config.walletAddress}`); + + await this.processUnsentAlerts(); + } + + private async processUnsentAlerts(): Promise { + try { + const unsentAlerts = await this.policyRepository.getUnsentAlerts(); + + if (unsentAlerts.length === 0) { + return; + } + + this.logger.info(`Processing ${unsentAlerts.length} unsent alerts`); + + for (const alert of unsentAlerts) { + try { + await this.sendAlert(alert); + await this.policyRepository.markAlertSent(alert.id); + } catch (error) { + this.logger.error(`Failed to send alert for violation ${alert.id}`, error); + } + } + } catch (error) { + this.logger.error('Error processing unsent alerts', error); + } + } + + private async sendAlert(violation: any): Promise { + this.logger.info( + `Sending alert for violation ${violation.id} - policy: ${violation.policyName}, rule: ${violation.violatedRule}`, + ); + } +} diff --git a/src/modules/treasury/policies/treasury-policy.service.ts b/src/modules/treasury/policies/treasury-policy.service.ts new file mode 100644 index 0000000..e5b4fb4 --- /dev/null +++ b/src/modules/treasury/policies/treasury-policy.service.ts @@ -0,0 +1,222 @@ +import { Logger } from '../../../utils/logger'; +import { TreasuryPolicyRepository } from './treasury-policy.repository'; +import { TreasuryPolicyValidator } from './treasury-policy.validator'; +import { TreasuryPolicyEngine } from './treasury-policy.engine'; +import { PolicyEvaluationResult } from './interfaces'; +import { TransactionData } from './interfaces/transaction-data.interface'; +import { ITreasuryPolicyService } from './interfaces/treasury-policy-service.interface'; +import { + CreateTreasuryPolicyDto, + UpdateTreasuryPolicyDto, + TreasuryPolicyQueryDto, + ViolationQueryDto, +} from './dto'; +import { TreasuryPolicyEntity } from './entities/treasury-policy.entity'; + +export class TreasuryPolicyService implements ITreasuryPolicyService { + private logger: Logger; + private validator: TreasuryPolicyValidator; + private engine: TreasuryPolicyEngine; + + constructor(private policyRepository: TreasuryPolicyRepository) { + this.logger = new Logger('TreasuryPolicyService'); + this.validator = new TreasuryPolicyValidator(); + this.engine = new TreasuryPolicyEngine(); + } + + async evaluateTransaction(transaction: TransactionData): Promise { + this.logger.debug(`Evaluating transaction ${transaction.hash} for wallet ${transaction.from}`); + + const policies = await this.policyRepository.getActivePolicies(transaction.from, transaction.chainId); + + if (policies.length === 0) { + this.logger.debug(`No active policies found for wallet ${transaction.from}`); + return []; + } + + const results = await this.engine.evaluateAll(transaction, policies); + + for (const result of results) { + if (result.violated) { + await this.createViolationFromResult(result, transaction); + } + } + + return results; + } + + async evaluatePoliciesForWallet(walletAddress: string, chainId: number): Promise { + this.logger.debug(`Evaluating policies for wallet ${walletAddress} on chain ${chainId}`); + + const policies = await this.policyRepository.getActivePolicies(walletAddress, chainId); + + if (policies.length === 0) { + return []; + } + + const results: PolicyEvaluationResult[] = []; + + for (const policy of policies) { + const result: PolicyEvaluationResult = { + policyId: policy.id, + policyName: policy.policyName, + walletAddress: policy.walletAddress, + chainId: policy.chainId, + transactionHash: 'N/A', + violated: false, + }; + results.push(result); + } + + return results; + } + + async getActivePolicies(walletAddress: string, chainId: number): Promise { + return this.policyRepository.getActivePolicies(walletAddress, chainId); + } + + async createPolicy(dto: CreateTreasuryPolicyDto, createdBy?: string): Promise { + const validation = this.validator.validateCreatePolicyDto(dto); + + if (!validation.valid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + const thresholdConfigValidation = this.validator.validateThresholdConfig(dto.thresholdConfig); + if (!thresholdConfigValidation.valid) { + throw new Error(`Threshold config validation failed: ${thresholdConfigValidation.errors.join(', ')}`); + } + + const policy = await this.policyRepository.createPolicy(dto); + + if (createdBy) { + await this.policyRepository.createAuditLog( + policy.id, + 'CREATE', + createdBy, + undefined, + undefined, + dto as unknown as Record, + 'Policy created', + ); + } + + this.logger.info(`Created policy ${policy.policyName} for wallet ${policy.walletAddress}`); + return policy; + } + + async updatePolicy( + id: string, + dto: UpdateTreasuryPolicyDto, + updatedBy?: string, + reason?: string, + ): Promise { + const validation = this.validator.validateUpdatePolicyDto(dto); + + if (!validation.valid) { + throw new Error(`Validation failed: ${validation.errors.join(', ')}`); + } + + const existingPolicy = await this.policyRepository.getPolicyById(id); + if (!existingPolicy) { + throw new Error('Policy not found'); + } + + const updatedPolicy = await this.policyRepository.updatePolicy(id, dto); + + if (updatedBy && updatedPolicy) { + await this.policyRepository.createAuditLog( + id, + 'UPDATE', + updatedBy, + dto as unknown as Record, + existingPolicy as unknown as Record, + updatedPolicy as unknown as Record, + reason, + ); + } + + this.logger.info(`Updated policy ${existingPolicy.policyName}`); + return updatedPolicy; + } + + async deletePolicy(id: string, deletedBy?: string, reason?: string): Promise { + const existingPolicy = await this.policyRepository.getPolicyById(id); + if (!existingPolicy) { + throw new Error('Policy not found'); + } + + const result = await this.policyRepository.deletePolicy(id); + + if (deletedBy) { + await this.policyRepository.createAuditLog( + id, + 'DELETE', + deletedBy, + undefined, + existingPolicy as unknown as Record, + undefined, + reason, + ); + } + + this.logger.info(`Deleted policy ${existingPolicy.policyName}`); + return result; + } + + async getPolicyById(id: string): Promise { + return this.policyRepository.getPolicyById(id); + } + + async getPolicies(query: TreasuryPolicyQueryDto): Promise<{ items: TreasuryPolicyEntity[]; total: number }> { + return this.policyRepository.getPolicies(query); + } + + async getViolations(query: ViolationQueryDto): Promise<{ items: any[]; total: number }> { + return this.policyRepository.getViolations(query); + } + + async acknowledgeViolation(id: string, acknowledgedBy: string): Promise { + await this.policyRepository.updateViolationStatus(id, 'ACKNOWLEDGED', acknowledgedBy); + this.logger.info(`Acknowledged violation ${id}`); + } + + async resolveViolation(id: string, resolvedBy: string, notes?: string): Promise { + await this.policyRepository.updateViolationStatus(id, 'RESOLVED', undefined, resolvedBy, notes); + this.logger.info(`Resolved violation ${id}`); + } + + async markAsFalsePositive(id: string, resolvedBy: string, notes?: string): Promise { + await this.policyRepository.updateViolationStatus(id, 'FALSE_POSITIVE', undefined, resolvedBy, notes); + this.logger.info(`Marked violation ${id} as false positive`); + } + + async getAuditLogs(policyId: string, limit = 50): Promise { + return this.policyRepository.getAuditLogs(policyId, limit); + } + + private async createViolationFromResult(result: PolicyEvaluationResult, transaction: TransactionData): Promise { + const violationDto = { + policyId: result.policyId, + policyName: result.policyName, + walletAddress: result.walletAddress, + chainId: result.chainId, + transactionHash: result.transactionHash, + transactionBlock: transaction.blockNumber.toString(), + violatedRule: result.violatedRule || '', + thresholdValue: result.thresholdValue || '0', + observedValue: result.observedValue || '0', + transactionAmount: transaction.value, + recipientAddress: transaction.to, + balanceBefore: transaction.balanceBefore, + balanceAfter: transaction.balanceAfter, + severity: (result.severity || 'HIGH') as any, + status: 'ACTIVE' as any, + recommendedAction: result.recommendedAction, + contextData: result.contextData, + }; + + await this.policyRepository.createViolation(violationDto); + this.logger.info(`Created violation for policy ${result.policyName}`); + } +} diff --git a/src/modules/treasury/policies/treasury-policy.validator.ts b/src/modules/treasury/policies/treasury-policy.validator.ts new file mode 100644 index 0000000..a233994 --- /dev/null +++ b/src/modules/treasury/policies/treasury-policy.validator.ts @@ -0,0 +1,148 @@ +import { CreateTreasuryPolicyDto, UpdateTreasuryPolicyDto } from './dto'; +import { PolicyType, ThresholdType } from './enums'; +import { Logger } from '../../../utils/logger'; + +export class TreasuryPolicyValidator { + private logger: Logger; + + constructor() { + this.logger = new Logger('TreasuryPolicyValidator'); + } + + validateCreatePolicyDto(dto: CreateTreasuryPolicyDto): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!dto.policyName || dto.policyName.trim().length === 0) { + errors.push('Policy name is required'); + } + + if (!dto.walletAddress || !this.isValidAddress(dto.walletAddress)) { + errors.push('Invalid wallet address'); + } + + if (!dto.chainId || dto.chainId <= 0) { + errors.push('Invalid chain ID'); + } + + if (!dto.policyType) { + errors.push('Policy type is required'); + } + + if (!dto.thresholdType) { + errors.push('Threshold type is required'); + } + + if (!dto.thresholdValue || dto.thresholdValue.trim().length === 0) { + errors.push('Threshold value is required'); + } else { + const thresholdNum = parseFloat(dto.thresholdValue); + if (isNaN(thresholdNum) || thresholdNum < 0) { + errors.push('Threshold value must be a positive number'); + } + } + + const validationErrors = this.validatePolicyTypeSpecificRules(dto); + errors.push(...validationErrors); + + return { + valid: errors.length === 0, + errors, + }; + } + + validateUpdatePolicyDto(dto: UpdateTreasuryPolicyDto): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (dto.thresholdValue !== undefined) { + const thresholdNum = parseFloat(dto.thresholdValue); + if (isNaN(thresholdNum) || thresholdNum < 0) { + errors.push('Threshold value must be a positive number'); + } + } + + return { + valid: errors.length === 0, + errors, + }; + } + + private validatePolicyTypeSpecificRules(dto: CreateTreasuryPolicyDto): string[] { + const errors: string[] = []; + + switch (dto.policyType) { + case PolicyType.UnauthorizedDestination: + if (!dto.approvedAddresses || dto.approvedAddresses.length === 0) { + errors.push('Unauthorized destination policy requires at least one approved address'); + } + break; + + case PolicyType.BusinessHoursOnly: + if (!dto.businessHoursStart || !dto.businessHoursEnd) { + errors.push('Business hours policy requires start and end times'); + } + if (!dto.timeZone) { + errors.push('Business hours policy requires time zone'); + } + break; + + case PolicyType.DailyTransferLimit: + case PolicyType.HourlyTransferLimit: + case PolicyType.MaxTransactionCount: + if (dto.thresholdType !== ThresholdType.Count && dto.thresholdType !== ThresholdType.FixedAmount) { + errors.push(`${dto.policyType} requires threshold type of Count or FixedAmount`); + } + break; + + case PolicyType.BalancePercentageTransfer: + if (dto.thresholdType !== ThresholdType.Percentage) { + errors.push('Balance percentage transfer requires threshold type of Percentage'); + } + const percentage = parseFloat(dto.thresholdValue); + if (percentage > 100) { + errors.push('Percentage threshold cannot exceed 100'); + } + break; + + case PolicyType.MaxTransactionAmount: + case PolicyType.LargeBalanceDecrease: + case PolicyType.MinBalanceThreshold: + if (dto.thresholdType !== ThresholdType.FixedAmount) { + errors.push(`${dto.policyType} requires threshold type of FixedAmount`); + } + break; + } + + return errors; + } + + private isValidAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/.test(address); + } + + validateThresholdConfig(config: Record | undefined): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!config) { + return { valid: true, errors }; + } + + if (config.timeWindow !== undefined) { + const timeWindow = Number(config.timeWindow); + if (isNaN(timeWindow) || timeWindow <= 0) { + errors.push('Time window must be a positive number'); + } + } + + if (config.riskScore !== undefined) { + const riskScore = Number(config.riskScore); + if (isNaN(riskScore) || riskScore < 0 || riskScore > 100) { + errors.push('Risk score must be between 0 and 100'); + } + } + + return { + valid: errors.length === 0, + errors, + }; + } +} diff --git a/src/modules/treasury/policies/utils/address.util.ts b/src/modules/treasury/policies/utils/address.util.ts new file mode 100644 index 0000000..250a764 --- /dev/null +++ b/src/modules/treasury/policies/utils/address.util.ts @@ -0,0 +1,12 @@ +export function isValidEthereumAddress(address: string): boolean { + return /^0x[a-fA-F0-9]{40}$/.test(address); +} + +export function normalizeAddress(address: string): string { + return address.toLowerCase(); +} + +export function truncateAddress(address: string, length = 6): string { + if (!address || address.length < 10) return address; + return `${address.substring(0, length)}...${address.substring(address.length - 4)}`; +} diff --git a/src/modules/treasury/policies/utils/index.ts b/src/modules/treasury/policies/utils/index.ts new file mode 100644 index 0000000..d5ed4dd --- /dev/null +++ b/src/modules/treasury/policies/utils/index.ts @@ -0,0 +1,2 @@ +export * from './address.util'; +export * from './time.util'; diff --git a/src/modules/treasury/policies/utils/time.util.ts b/src/modules/treasury/policies/utils/time.util.ts new file mode 100644 index 0000000..5fcb1cd --- /dev/null +++ b/src/modules/treasury/policies/utils/time.util.ts @@ -0,0 +1,39 @@ +export function isWithinBusinessHours( + transactionTime: Date, + startTime: string, + endTime: string, + timeZone: string, +): boolean { + try { + const options: Intl.DateTimeFormatOptions = { + timeZone, + hour: '2-digit', + minute: '2-digit', + hour12: false, + }; + + const formatter = new Intl.DateTimeFormat('en-US', options); + const timeStr = formatter.format(transactionTime); + const [hours, minutes] = timeStr.split(':').map(Number); + const transactionMinutes = hours * 60 + minutes; + + const [startHours, startMinutes] = startTime.split(':').map(Number); + const [endHours, endMinutes] = endTime.split(':').map(Number); + const startMinutesTotal = startHours * 60 + startMinutes; + const endMinutesTotal = endHours * 60 + endMinutes; + + return transactionMinutes >= startMinutesTotal && transactionMinutes <= endMinutesTotal; + } catch (error) { + return true; + } +} + +export function getTimeWindowStart(endTime: Date, hours: number): Date { + const startTime = new Date(endTime); + startTime.setHours(startTime.getHours() - hours); + return startTime; +} + +export function formatTimestamp(timestamp: Date): string { + return timestamp.toISOString(); +} From 03148d37e7e87445c358db5deb6cfdef9ca02486 Mon Sep 17 00:00:00 2001 From: Ogunmodede Joel Taiwo Date: Sat, 18 Jul 2026 14:48:50 +0100 Subject: [PATCH 2/2] fix: resolve linting errors in treasury policies module - Apply prettier auto-formatting - Fix unused parameter warnings by prefixing with underscore --- .../policies/alerts/alert-generator.util.ts | 2 +- .../entities/policy-audit-log.entity.ts | 8 +-- .../treasury-policy-service.interface.ts | 5 +- .../policies/treasury-policy.engine.ts | 72 +++++++++++++++---- .../policies/treasury-policy.repository.ts | 17 +++-- .../policies/treasury-policy.scheduler.ts | 14 +++- .../policies/treasury-policy.service.ts | 36 ++++++++-- .../policies/treasury-policy.validator.ts | 10 ++- 8 files changed, 124 insertions(+), 40 deletions(-) diff --git a/src/modules/treasury/policies/alerts/alert-generator.util.ts b/src/modules/treasury/policies/alerts/alert-generator.util.ts index b83bf31..d362552 100644 --- a/src/modules/treasury/policies/alerts/alert-generator.util.ts +++ b/src/modules/treasury/policies/alerts/alert-generator.util.ts @@ -36,7 +36,7 @@ export function generateAlertForViolation(violation: PolicyViolationEntity): Ale export function formatAlertMessage(alert: AlertData): string { const severityEmoji = getSeverityEmoji(alert.severity); const title = `${severityEmoji} Treasury Policy Violation: ${alert.policyName}`; - + const details = [ `Wallet: ${alert.walletAddress}`, `Chain: ${alert.chainId}`, diff --git a/src/modules/treasury/policies/entities/policy-audit-log.entity.ts b/src/modules/treasury/policies/entities/policy-audit-log.entity.ts index de28c63..b383481 100644 --- a/src/modules/treasury/policies/entities/policy-audit-log.entity.ts +++ b/src/modules/treasury/policies/entities/policy-audit-log.entity.ts @@ -1,10 +1,4 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - Index, - CreateDateColumn, -} from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, Index, CreateDateColumn } from 'typeorm'; @Entity('policy_audit_logs') @Index(['policyId']) diff --git a/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts b/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts index 34f9016..6a81fd3 100644 --- a/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts +++ b/src/modules/treasury/policies/interfaces/treasury-policy-service.interface.ts @@ -3,7 +3,10 @@ import { TransactionData } from './transaction-data.interface'; export interface ITreasuryPolicyService { evaluateTransaction(transaction: TransactionData): Promise; - evaluatePoliciesForWallet(walletAddress: string, chainId: number): Promise; + evaluatePoliciesForWallet( + walletAddress: string, + chainId: number, + ): Promise; getActivePolicies(walletAddress: string, chainId: number): Promise; } diff --git a/src/modules/treasury/policies/treasury-policy.engine.ts b/src/modules/treasury/policies/treasury-policy.engine.ts index ac1a796..091951e 100644 --- a/src/modules/treasury/policies/treasury-policy.engine.ts +++ b/src/modules/treasury/policies/treasury-policy.engine.ts @@ -12,7 +12,10 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { this.logger = new Logger('TreasuryPolicyEngine'); } - async evaluate(transaction: TransactionData, policy: TreasuryPolicyEntity): Promise { + async evaluate( + transaction: TransactionData, + policy: TreasuryPolicyEntity, + ): Promise { this.logger.debug(`Evaluating policy ${policy.policyName} for transaction ${transaction.hash}`); const result: PolicyEvaluationResult = { @@ -70,7 +73,10 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { return result; } - async evaluateAll(transaction: TransactionData, policies: TreasuryPolicyEntity[]): Promise { + async evaluateAll( + transaction: TransactionData, + policies: TreasuryPolicyEntity[], + ): Promise { this.logger.debug(`Evaluating ${policies.length} policies for transaction ${transaction.hash}`); const results: PolicyEvaluationResult[] = []; @@ -137,7 +143,11 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { result: PolicyEvaluationResult, ): Promise { const threshold = BigInt(policy.thresholdValue); - const hourlyTotal = await this.calculateTotalTransfersInWindow(policy.walletAddress, policy.chainId, 1); + const hourlyTotal = await this.calculateTotalTransfersInWindow( + policy.walletAddress, + policy.chainId, + 1, + ); if (hourlyTotal > threshold) { result.violated = true; @@ -158,7 +168,11 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { const timeWindow = policy.thresholdConfig?.timeWindow as number | undefined; const hours = timeWindow || 24; - const transactionCount = await this.countTransactionsInWindow(policy.walletAddress, policy.chainId, hours); + const transactionCount = await this.countTransactionsInWindow( + policy.walletAddress, + policy.chainId, + hours, + ); if (transactionCount > threshold) { result.violated = true; @@ -248,7 +262,11 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { const timeWindow = policy.thresholdConfig?.timeWindow as number | undefined; const minutes = timeWindow || 60; - const transactionCount = await this.countTransactionsInWindow(policy.walletAddress, policy.chainId, minutes / 60); + const transactionCount = await this.countTransactionsInWindow( + policy.walletAddress, + policy.chainId, + minutes / 60, + ); if (transactionCount > threshold) { result.violated = true; @@ -266,7 +284,11 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { policy: TreasuryPolicyEntity, result: PolicyEvaluationResult, ): Promise { - const isFirstTime = await this.isFirstTimeRecipient(policy.walletAddress, transaction.to, policy.chainId); + const isFirstTime = await this.isFirstTimeRecipient( + policy.walletAddress, + transaction.to, + policy.chainId, + ); if (isFirstTime) { result.violated = true; @@ -289,7 +311,12 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { } const transactionTime = transaction.timestamp; - const isBusinessHours = this.isWithinBusinessHours(transactionTime, policy.businessHoursStart, policy.businessHoursEnd, policy.timeZone); + const isBusinessHours = this.isWithinBusinessHours( + transactionTime, + policy.businessHoursStart, + policy.businessHoursEnd, + policy.timeZone, + ); if (!isBusinessHours) { result.violated = true; @@ -298,7 +325,10 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { result.observedValue = transactionTime.toISOString(); result.severity = AlertSeverity.Medium; result.recommendedAction = 'Review transaction outside business hours'; - result.contextData = { transactionTime: transactionTime.toISOString(), timeZone: policy.timeZone }; + result.contextData = { + transactionTime: transactionTime.toISOString(), + timeZone: policy.timeZone, + }; } } @@ -320,23 +350,35 @@ export class TreasuryPolicyEngine implements ITreasuryPolicyEngine { } } - private async calculateTotalTransfersInWindow(walletAddress: string, chainId: number, hours: number): Promise { + private async calculateTotalTransfersInWindow( + _walletAddress: string, + _chainId: number, + _hours: number, + ): Promise { return BigInt(0); } - private async countTransactionsInWindow(walletAddress: string, chainId: number, hours: number): Promise { + private async countTransactionsInWindow( + _walletAddress: string, + _chainId: number, + _hours: number, + ): Promise { return 0; } - private async isFirstTimeRecipient(walletAddress: string, recipient: string, chainId: number): Promise { + private async isFirstTimeRecipient( + _walletAddress: string, + _recipient: string, + _chainId: number, + ): Promise { return true; } private isWithinBusinessHours( - transactionTime: Date, - startTime: string, - endTime: string, - timeZone: string, + _transactionTime: Date, + _startTime: string, + _endTime: string, + _timeZone: string, ): boolean { return true; } diff --git a/src/modules/treasury/policies/treasury-policy.repository.ts b/src/modules/treasury/policies/treasury-policy.repository.ts index 6b299a5..140e97e 100644 --- a/src/modules/treasury/policies/treasury-policy.repository.ts +++ b/src/modules/treasury/policies/treasury-policy.repository.ts @@ -31,7 +31,10 @@ export class TreasuryPolicyRepository { return this.policyRepo.save(policy); } - async updatePolicy(id: string, dto: UpdateTreasuryPolicyDto): Promise { + async updatePolicy( + id: string, + dto: UpdateTreasuryPolicyDto, + ): Promise { await this.policyRepo.update(id, dto); return this.policyRepo.findOne({ where: { id } }); } @@ -45,7 +48,9 @@ export class TreasuryPolicyRepository { return this.policyRepo.findOne({ where: { id } }); } - async getPolicies(query: TreasuryPolicyQueryDto): Promise<{ items: TreasuryPolicyEntity[]; total: number }> { + async getPolicies( + query: TreasuryPolicyQueryDto, + ): Promise<{ items: TreasuryPolicyEntity[]; total: number }> { const qb = this.policyRepo.createQueryBuilder('tp'); if (query.walletAddress) { @@ -111,7 +116,9 @@ export class TreasuryPolicyRepository { return this.violationRepo.findOne({ where: { id } }); } - async getViolations(query: ViolationQueryDto): Promise<{ items: PolicyViolationEntity[]; total: number }> { + async getViolations( + query: ViolationQueryDto, + ): Promise<{ items: PolicyViolationEntity[]; total: number }> { const qb = this.violationRepo.createQueryBuilder('pv'); if (query.policyId) { @@ -124,7 +131,9 @@ export class TreasuryPolicyRepository { qb.andWhere('pv.chainId = :chainId', { chainId: query.chainId }); } if (query.transactionHash) { - qb.andWhere('pv.transactionHash = :transactionHash', { transactionHash: query.transactionHash }); + qb.andWhere('pv.transactionHash = :transactionHash', { + transactionHash: query.transactionHash, + }); } if (query.status) { qb.andWhere('pv.status = :status', { status: query.status }); diff --git a/src/modules/treasury/policies/treasury-policy.scheduler.ts b/src/modules/treasury/policies/treasury-policy.scheduler.ts index a6bed16..e4b0c8f 100644 --- a/src/modules/treasury/policies/treasury-policy.scheduler.ts +++ b/src/modules/treasury/policies/treasury-policy.scheduler.ts @@ -69,7 +69,10 @@ export class TreasuryPolicyScheduler implements ITreasuryPolicyScheduler { try { await this.monitorWallet(config); } catch (error) { - this.logger.error(`Error monitoring wallet ${config.walletAddress} on chain ${config.chainId}`, error); + this.logger.error( + `Error monitoring wallet ${config.walletAddress} on chain ${config.chainId}`, + error, + ); } }, pollInterval); @@ -79,14 +82,19 @@ export class TreasuryPolicyScheduler implements ITreasuryPolicyScheduler { private async monitorWallet(config: TreasuryPolicyConfig): Promise { this.logger.debug(`Monitoring wallet ${config.walletAddress} on chain ${config.chainId}`); - const policies = await this.policyRepository.getActivePolicies(config.walletAddress, config.chainId); + const policies = await this.policyRepository.getActivePolicies( + config.walletAddress, + config.chainId, + ); if (policies.length === 0) { this.logger.debug(`No active policies for wallet ${config.walletAddress}`); return; } - this.logger.debug(`Found ${policies.length} active policies for wallet ${config.walletAddress}`); + this.logger.debug( + `Found ${policies.length} active policies for wallet ${config.walletAddress}`, + ); await this.processUnsentAlerts(); } diff --git a/src/modules/treasury/policies/treasury-policy.service.ts b/src/modules/treasury/policies/treasury-policy.service.ts index e5b4fb4..1510e3d 100644 --- a/src/modules/treasury/policies/treasury-policy.service.ts +++ b/src/modules/treasury/policies/treasury-policy.service.ts @@ -27,7 +27,10 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { async evaluateTransaction(transaction: TransactionData): Promise { this.logger.debug(`Evaluating transaction ${transaction.hash} for wallet ${transaction.from}`); - const policies = await this.policyRepository.getActivePolicies(transaction.from, transaction.chainId); + const policies = await this.policyRepository.getActivePolicies( + transaction.from, + transaction.chainId, + ); if (policies.length === 0) { this.logger.debug(`No active policies found for wallet ${transaction.from}`); @@ -45,7 +48,10 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { return results; } - async evaluatePoliciesForWallet(walletAddress: string, chainId: number): Promise { + async evaluatePoliciesForWallet( + walletAddress: string, + chainId: number, + ): Promise { this.logger.debug(`Evaluating policies for wallet ${walletAddress} on chain ${chainId}`); const policies = await this.policyRepository.getActivePolicies(walletAddress, chainId); @@ -75,7 +81,10 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { return this.policyRepository.getActivePolicies(walletAddress, chainId); } - async createPolicy(dto: CreateTreasuryPolicyDto, createdBy?: string): Promise { + async createPolicy( + dto: CreateTreasuryPolicyDto, + createdBy?: string, + ): Promise { const validation = this.validator.validateCreatePolicyDto(dto); if (!validation.valid) { @@ -84,7 +93,9 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { const thresholdConfigValidation = this.validator.validateThresholdConfig(dto.thresholdConfig); if (!thresholdConfigValidation.valid) { - throw new Error(`Threshold config validation failed: ${thresholdConfigValidation.errors.join(', ')}`); + throw new Error( + `Threshold config validation failed: ${thresholdConfigValidation.errors.join(', ')}`, + ); } const policy = await this.policyRepository.createPolicy(dto); @@ -168,7 +179,9 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { return this.policyRepository.getPolicyById(id); } - async getPolicies(query: TreasuryPolicyQueryDto): Promise<{ items: TreasuryPolicyEntity[]; total: number }> { + async getPolicies( + query: TreasuryPolicyQueryDto, + ): Promise<{ items: TreasuryPolicyEntity[]; total: number }> { return this.policyRepository.getPolicies(query); } @@ -187,7 +200,13 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { } async markAsFalsePositive(id: string, resolvedBy: string, notes?: string): Promise { - await this.policyRepository.updateViolationStatus(id, 'FALSE_POSITIVE', undefined, resolvedBy, notes); + await this.policyRepository.updateViolationStatus( + id, + 'FALSE_POSITIVE', + undefined, + resolvedBy, + notes, + ); this.logger.info(`Marked violation ${id} as false positive`); } @@ -195,7 +214,10 @@ export class TreasuryPolicyService implements ITreasuryPolicyService { return this.policyRepository.getAuditLogs(policyId, limit); } - private async createViolationFromResult(result: PolicyEvaluationResult, transaction: TransactionData): Promise { + private async createViolationFromResult( + result: PolicyEvaluationResult, + transaction: TransactionData, + ): Promise { const violationDto = { policyId: result.policyId, policyName: result.policyName, diff --git a/src/modules/treasury/policies/treasury-policy.validator.ts b/src/modules/treasury/policies/treasury-policy.validator.ts index a233994..a0cb818 100644 --- a/src/modules/treasury/policies/treasury-policy.validator.ts +++ b/src/modules/treasury/policies/treasury-policy.validator.ts @@ -88,7 +88,10 @@ export class TreasuryPolicyValidator { case PolicyType.DailyTransferLimit: case PolicyType.HourlyTransferLimit: case PolicyType.MaxTransactionCount: - if (dto.thresholdType !== ThresholdType.Count && dto.thresholdType !== ThresholdType.FixedAmount) { + if ( + dto.thresholdType !== ThresholdType.Count && + dto.thresholdType !== ThresholdType.FixedAmount + ) { errors.push(`${dto.policyType} requires threshold type of Count or FixedAmount`); } break; @@ -119,7 +122,10 @@ export class TreasuryPolicyValidator { return /^0x[a-fA-F0-9]{40}$/.test(address); } - validateThresholdConfig(config: Record | undefined): { valid: boolean; errors: string[] } { + validateThresholdConfig(config: Record | undefined): { + valid: boolean; + errors: string[]; + } { const errors: string[] = []; if (!config) {