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
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* src/modules/reputation/constants/reputation.constants.ts
*
* Central place for every magic number used by the reputation engine.
* Keeping these here (instead of scattered through the services) makes
* the scoring model auditable and easy to tune without touching logic.
*/

/** Composite score is always normalized to this range. */
export const REPUTATION_SCORE_MIN = 0;
export const REPUTATION_SCORE_MAX = 100;

/**
* Relative weight of each factor in the final composite score.
* Must sum to 1 — enforced by a unit test in reputation.service.spec.ts.
*/
export const REPUTATION_WEIGHTS = {
walletAge: 0.15,
activityConsistency: 0.2,
riskIndicators: 0.35,
incidentHistory: 0.2,
networkAssociation: 0.1,
} as const;

/** How long a computed score is considered fresh before a recalculation is due. */
export const REPUTATION_SCORE_TTL_MS = 1000 * 60 * 60 * 6; // 6 hours

/** Trust tiers derived from the final composite score. */
export enum ReputationTier {
TRUSTED = 'TRUSTED',
NEUTRAL = 'NEUTRAL',
SUSPICIOUS = 'SUSPICIOUS',
HIGH_RISK = 'HIGH_RISK',
}

export const REPUTATION_TIER_THRESHOLDS: { tier: ReputationTier; min: number }[] = [
{ tier: ReputationTier.TRUSTED, min: 75 },
{ tier: ReputationTier.NEUTRAL, min: 50 },
{ tier: ReputationTier.SUSPICIOUS, min: 25 },
{ tier: ReputationTier.HIGH_RISK, min: 0 },
];

/** Severity levels used by individual risk indicators. */
export enum RiskIndicatorSeverity {
INFO = 'INFO',
LOW = 'LOW',
MEDIUM = 'MEDIUM',
HIGH = 'HIGH',
CRITICAL = 'CRITICAL',
}

/** Point deduction applied to the risk-indicator sub-score per severity hit. */
export const RISK_SEVERITY_PENALTY: Record<RiskIndicatorSeverity, number> = {
[RiskIndicatorSeverity.INFO]: 0,
[RiskIndicatorSeverity.LOW]: 5,
[RiskIndicatorSeverity.MEDIUM]: 15,
[RiskIndicatorSeverity.HIGH]: 30,
[RiskIndicatorSeverity.CRITICAL]: 50,
};

/** Max number of hops used when walking the wallet's association graph. */
export const NETWORK_ANALYSIS_MAX_HOPS = 2;

/** Max number of historical snapshots returned by default in the history endpoint. */
export const DEFAULT_HISTORY_PAGE_SIZE = 20;
export const MAX_HISTORY_PAGE_SIZE = 100;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Type } from 'class-transformer';
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
import { DEFAULT_HISTORY_PAGE_SIZE, MAX_HISTORY_PAGE_SIZE } from '../constants/reputation.constants';

Check failure on line 3 in apps/backend/src/modules/reputations/dto/query-reputation-history.dto.ts

View workflow job for this annotation

GitHub Actions / Linting (22.x)

Replace `·DEFAULT_HISTORY_PAGE_SIZE,·MAX_HISTORY_PAGE_SIZE·` with `⏎··DEFAULT_HISTORY_PAGE_SIZE,⏎··MAX_HISTORY_PAGE_SIZE,⏎`

Check failure on line 3 in apps/backend/src/modules/reputations/dto/query-reputation-history.dto.ts

View workflow job for this annotation

GitHub Actions / Linting (20.x)

Replace `·DEFAULT_HISTORY_PAGE_SIZE,·MAX_HISTORY_PAGE_SIZE·` with `⏎··DEFAULT_HISTORY_PAGE_SIZE,⏎··MAX_HISTORY_PAGE_SIZE,⏎`

export class QueryReputationHistoryDto {
@IsOptional()
@IsString()
chainId?: string;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(MAX_HISTORY_PAGE_SIZE)
limit: number = DEFAULT_HISTORY_PAGE_SIZE;

@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
offset: number = 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsBoolean, IsOptional, IsString } from 'class-validator';

export class RecalculateReputationDto {
@IsString()
chainId: string;

Check failure on line 5 in apps/backend/src/modules/reputations/dto/recalculate-reputation.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'chainId' has no initializer and is not definitely assigned in the constructor.

Check failure on line 5 in apps/backend/src/modules/reputations/dto/recalculate-reputation.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'chainId' has no initializer and is not definitely assigned in the constructor.

Check failure on line 5 in apps/backend/src/modules/reputations/dto/recalculate-reputation.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'chainId' has no initializer and is not definitely assigned in the constructor.

/**
* Bypasses the TTL cache check and forces a fresh calculation even if a
* recent score already exists. Defaults to false to avoid needlessly
* hammering the chain-data integrations on every request.
*/
@IsOptional()
@IsBoolean()
force?: boolean = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Exclude, Expose, Type } from 'class-transformer';
import { ReputationTier } from '../constants/reputation.constants';
import { ReputationBreakdown, RiskIndicator } from '../interfaces/reputation.interfaces';

@Exclude()
export class ReputationScoreResponseDto {
@Expose()
walletAddress: string;

Check failure on line 8 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'walletAddress' has no initializer and is not definitely assigned in the constructor.

Check failure on line 8 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'walletAddress' has no initializer and is not definitely assigned in the constructor.

Check failure on line 8 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'walletAddress' has no initializer and is not definitely assigned in the constructor.

@Expose()
chainId: string;

Check failure on line 11 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'chainId' has no initializer and is not definitely assigned in the constructor.

Check failure on line 11 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'chainId' has no initializer and is not definitely assigned in the constructor.

Check failure on line 11 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'chainId' has no initializer and is not definitely assigned in the constructor.

@Expose()
score: number;

Check failure on line 14 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'score' has no initializer and is not definitely assigned in the constructor.

Check failure on line 14 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'score' has no initializer and is not definitely assigned in the constructor.

Check failure on line 14 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'score' has no initializer and is not definitely assigned in the constructor.

@Expose()
tier: ReputationTier;

Check failure on line 17 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'tier' has no initializer and is not definitely assigned in the constructor.

Check failure on line 17 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'tier' has no initializer and is not definitely assigned in the constructor.

Check failure on line 17 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'tier' has no initializer and is not definitely assigned in the constructor.

@Expose()
breakdown: ReputationBreakdown;

Check failure on line 20 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'breakdown' has no initializer and is not definitely assigned in the constructor.

Check failure on line 20 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'breakdown' has no initializer and is not definitely assigned in the constructor.

Check failure on line 20 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'breakdown' has no initializer and is not definitely assigned in the constructor.

@Expose()
indicators: RiskIndicator[];

Check failure on line 23 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'indicators' has no initializer and is not definitely assigned in the constructor.

Check failure on line 23 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'indicators' has no initializer and is not definitely assigned in the constructor.

Check failure on line 23 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'indicators' has no initializer and is not definitely assigned in the constructor.

@Expose()
@Type(() => Date)
calculatedAt: Date;

Check failure on line 27 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'calculatedAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 27 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'calculatedAt' has no initializer and is not definitely assigned in the constructor.

Check failure on line 27 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'calculatedAt' has no initializer and is not definitely assigned in the constructor.
}

@Exclude()
export class ReputationHistoryItemDto {
@Expose()
score: number;

Check failure on line 33 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'score' has no initializer and is not definitely assigned in the constructor.

Check failure on line 33 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'score' has no initializer and is not definitely assigned in the constructor.

Check failure on line 33 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'score' has no initializer and is not definitely assigned in the constructor.

@Expose()
tier: ReputationTier;

Check failure on line 36 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / TypeScript Type Checking

Property 'tier' has no initializer and is not definitely assigned in the constructor.

Check failure on line 36 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (22.x)

Property 'tier' has no initializer and is not definitely assigned in the constructor.

Check failure on line 36 in apps/backend/src/modules/reputations/dto/reputation-score-response.dto.ts

View workflow job for this annotation

GitHub Actions / Build (20.x)

Property 'tier' has no initializer and is not definitely assigned in the constructor.

@Expose()
@Type(() => Date)
calculatedAt: Date;
}

@Exclude()
export class PaginatedReputationHistoryDto {
@Expose()
@Type(() => ReputationHistoryItemDto)
items: ReputationHistoryItemDto[];

@Expose()
total: number;

@Expose()
limit: number;

@Expose()
offset: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* src/modules/reputation/interfaces/reputation.interfaces.ts
*/
import { ReputationTier, RiskIndicatorSeverity } from '../constants/reputation.constants';

export interface RiskIndicator {
code: string; // e.g. 'SANCTIONED_LIST_MATCH', 'MIXER_INTERACTION'
description: string;
severity: RiskIndicatorSeverity;
detectedAt: Date;
metadata?: Record<string, unknown>;
}

export interface FactorScore {
/** Raw score for this factor, normalized 0-100. */
score: number;
/** Weight applied when composing the final score. */
weight: number;
/** Human-readable notes explaining how the score was derived. */
details: string[];
}

export interface ReputationBreakdown {
walletAge: FactorScore;
activityConsistency: FactorScore;
riskIndicators: FactorScore;
incidentHistory: FactorScore;
networkAssociation: FactorScore;
}

export interface ReputationScoreResult {
walletAddress: string;
chainId: string;
score: number;
tier: ReputationTier;
breakdown: ReputationBreakdown;
indicators: RiskIndicator[];
calculatedAt: Date;
}

/**
* Minimal shape expected from the wallet's on-chain activity history.
* Populated by ChainsService / integrations module — see reputation.repository.ts.
*/
export interface WalletActivitySnapshot {
walletAddress: string;
chainId: string;
firstSeenAt: Date | null;
lastSeenAt: Date | null;
totalTransactions: number;
activeDaysLast90: number;
counterpartyAddresses: string[];
}

/**
* Minimal shape expected from the cases/incidents modules for a wallet.
*/
export interface WalletIncidentSummary {
walletAddress: string;
openIncidents: number;
resolvedIncidents: number;
confirmedFraudCases: number;
lastIncidentAt: Date | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* src/modules/reputation/providers/stub-chain-data.provider.ts
*
* TEMPORARY. Replace with a real adapter that calls the existing
* `chains` (and/or `integrations`) module to fetch actual wallet
* activity from indexed on-chain data. Kept here only so the reputation
* module is self-contained and testable until that integration lands.
*/
import { Injectable, Logger } from '@nestjs/common';
import { ChainDataProvider } from '../reputation.service';
import { WalletActivitySnapshot } from '../interfaces/reputation.interfaces';

@Injectable()
export class StubChainDataProvider implements ChainDataProvider {
private readonly logger = new Logger(StubChainDataProvider.name);

async getActivitySnapshot(
walletAddress: string,
chainId: string,
): Promise<WalletActivitySnapshot> {
this.logger.warn(
`StubChainDataProvider in use for ${walletAddress} — wire up the real ChainsService before deploying.`,
);

return {
walletAddress,
chainId,
firstSeenAt: null,
lastSeenAt: null,
totalTransactions: 0,
activeDaysLast90: 0,
counterpartyAddresses: [],
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* src/modules/reputation/providers/stub-incident-data.provider.ts
*
* TEMPORARY. Replace with a real adapter over the `cases` and
* `incidents` modules so wallet incident/fraud history feeds into the
* score. See PR description for the exact integration points expected.
*/
import { Injectable, Logger } from '@nestjs/common';
import { IncidentDataProvider } from '../reputation.service';
import { WalletIncidentSummary } from '../interfaces/reputation.interfaces';

@Injectable()
export class StubIncidentDataProvider implements IncidentDataProvider {
private readonly logger = new Logger(StubIncidentDataProvider.name);

async getIncidentSummary(walletAddress: string): Promise<WalletIncidentSummary> {
this.logger.warn(
`StubIncidentDataProvider in use for ${walletAddress} — wire up the real CasesService/IncidentsService before deploying.`,
);

return {
walletAddress,
openIncidents: 0,
resolvedIncidents: 0,
confirmedFraudCases: 0,
lastIncidentAt: null,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* src/modules/reputation/providers/stub-sanctions-list.provider.ts
*
* TEMPORARY. Replace with a real adapter over the platform's
* sanctioned/flagged address source (behavioral-analysis module, an
* OFAC feed, Chainalysis, etc). All checks default to "clean" so the
* module is safe to boot in dev/test without a live feed.
*/
import { Injectable, Logger } from '@nestjs/common';
import { SanctionsListProvider } from '../services/risk-indicator.service';

@Injectable()
export class StubSanctionsListProvider implements SanctionsListProvider {
private readonly logger = new Logger(StubSanctionsListProvider.name);
private warned = false;

async isSanctioned(_walletAddress: string): Promise<boolean> {
this.warnOnce();
return false;
}

async isKnownMixer(_walletAddress: string): Promise<boolean> {
this.warnOnce();
return false;
}

async isFlaggedContract(_walletAddress: string, _chainId: string): Promise<boolean> {
this.warnOnce();
return false;
}

private warnOnce() {
if (!this.warned) {
this.logger.warn(
'StubSanctionsListProvider in use — no sanctions/mixer/flagged-contract data source is wired up yet.',
);
this.warned = true;
}
}
}
Loading
Loading