Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions microservices/contract-registry-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM node:20-alpine AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /usr/src/app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main"]
14 changes: 14 additions & 0 deletions microservices/personalization-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM node:20-alpine AS builder
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /usr/src/app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main"]
19 changes: 19 additions & 0 deletions src/engine/personalization.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
import { PersonalizationService } from './personalization.service';

@Controller('personalization')
export class PersonalizationController {
constructor(private readonly personalizationService: PersonalizationService) {}

@Get('resolve/:userId')
async getUserRuntimeContext(@Param('userId') userId: string) {
return this.personalizationService.resolvePersonalizedContext(userId);
}

@Post('track')
async trackUserInteraction(
@Body() body: { userId: string; category: string; weight?: number },
) {
return this.personalizationService.recordInteraction(body.userId, body.category, body.weight);
}
}
83 changes: 83 additions & 0 deletions src/engine/personalization.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserPreference } from '../entities/preference.entity';
import { AbVariation } from '../entities/variation.entity';
import * as crypto from 'crypto';

@Injectable()
export class PersonalizationService {
constructor(
@InjectRepository(UserPreference) private readonly prefRepo: Repository<UserPreference>,
@InjectRepository(AbVariation) private readonly variantRepo: Repository<AbVariation>,
) {}

/**
* Learns and increments category affinity scores over time based on user interactions
*/
async recordInteraction(userId: string, category: string, weight: number = 1.0): Promise<UserPreference> {
let profile = await this.prefRepo.findOneBy({ userId });

if (!profile) {
profile = this.prefRepo.create({ userId, behavioralProfile: {}, categoryAffinities: {} });
}

const currentScore = profile.categoryAffinities[category] || 0;
profile.categoryAffinities[category] = currentScore + weight;

return this.prefRepo.save(profile);
}

/**
* Deterministically assigns a user variant based on a hash of their ID
*/
async getExperimentVariant(userId: string, experimentName: string): Promise<AbVariation> {
const variants = await this.variantRepo.findBy({ experimentName });
if (variants.length === 0) return null;

// Create a stable routing footprint using consistent hashing
const hash = crypto.createHash('md5').update(`${userId}:${experimentName}`).digest('hex');
const index = parseInt(hash.substring(0, 8), 16) % variants.length;

const assignedVariant = variants[index];

// Increment tracking impression count out-of-band
this.variantRepo.update(assignedVariant.id, { impressions: assignedVariant.impressions + 1 });

return assignedVariant;
}

/**
* Resolves tailored pricing structures, layouts, and recommended vault configurations
*/
async resolvePersonalizedContext(userId: string) {
const profile = await this.prefRepo.findOneBy({ userId });
const activeExperiment = await this.getExperimentVariant(userId, 'pricing_tier_optimization');

// Default Fallback Matrix
let pricingTier = 'tier_standard';
let layoutConfig = { density: 'default', showHighRiskVaults: false };

// Dynamic Preference Adjustments
if (profile) {
const highYieldAffinity = profile.categoryAffinities['high_yield'] || 0;
if (highYieldAffinity > 10) {
layoutConfig.showHighRiskVaults = true;
pricingTier = 'tier_preferred';
}
}

// Overwrites layout payload configs if caught in an active experiment loop
if (activeExperiment) {
pricingTier = activeExperiment.configurationPayload.pricingTier || pricingTier;
layoutConfig = { ...layoutConfig, ...activeExperiment.configurationPayload.layoutConfig };
}

return {
userId,
pricingTier,
layoutConfig,
experimentTrack: activeExperiment?.variantKey || 'none',
};
}
}
26 changes: 26 additions & 0 deletions src/entities/audit.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm';
import { SmartContract } from './contract.entity';

@Entity('security_audits')
export class SecurityAudit {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
auditorName: string;

@Column()
reportUrl: string;

@Column()
summary: string;

@Column()
findingsCount: number;

@ManyToOne(() => SmartContract, (c) => c.audits)
contract: SmartContract;

@CreateDateColumn()
createdAt: Date;
}
39 changes: 39 additions & 0 deletions src/entities/contract.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
import { ContractVersion } from './version.entity';
import { SecurityAudit } from './audit.entity';

export enum ContractStatus {
VERIFIED = 'verified',
UNVERIFIED = 'unverified',
DEPRECATED = 'deprecated',
}

@Entity('contracts')
export class SmartContract {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
contractId: string; // Stellar/Soroban Contract Address (e.g., C...)

@Column()
name: string;

@Column({ type: 'enum', enum: ContractStatus, default: ContractStatus.UNVERIFIED })
status: ContractStatus;

@Column({ nullable: true })
deprecationNotice: string;

@OneToMany(() => ContractVersion, (v) => v.contract, { cascade: true })
versions: ContractVersion[];

@OneToMany(() => SecurityAudit, (a) => a.contract, { cascade: true })
audits: SecurityAudit[];

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
24 changes: 24 additions & 0 deletions src/entities/preference.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('user_preferences')
export class UserPreference {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column({ unique: true })
userId: string;

// Storing dynamic matrices (e.g., favorite_vault_types, activity_level, UI_theme)
@Column({ type: 'jsonb', default: {} })
behavioralProfile: Record<string, any>;

// Calculated affinity scores used for ML matching loops
@Column({ type: 'jsonb', default: {} })
categoryAffinities: Record<string, number>;

@CreateDateColumn()
createdAt: Date;

@UpdateDateColumn()
updatedAt: Date;
}
25 changes: 25 additions & 0 deletions src/entities/variation.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';

@Entity('ab_variations')
export class AbVariation {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
experimentName: string; // e.g., "vault_pricing_tier_v2"

@Column()
variantKey: string; // e.g., "control", "variant_a", "variant_b"

@Column({ type: 'jsonb' })
configurationPayload: Record<string, any>; // Specific layout structure updates

@Column({ default: 0 })
impressions: number;

@Column({ default: 0 })
conversions: number;

@CreateDateColumn()
createdAt: Date;
}
26 changes: 26 additions & 0 deletions src/entities/version.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn } from 'typeorm';
import { SmartContract } from './contract.entity';

@Entity('contract_versions')
export class ContractVersion {
@PrimaryGeneratedColumn('uuid')
id: string;

@Column()
versionStr: string; // e.g., "1.0.0"

@Column()
wasmHash: string; // SHA256 hash of compiled WASM payload

@Column({ nullable: true })
sourceCodeUrl: string; // Reference link to verified GitHub commit

@Column({ default: false })
isVerified: boolean;

@ManyToOne(() => SmartContract, (c) => c.versions)
contract: SmartContract;

@CreateDateColumn()
createdAt: Date;
}
61 changes: 61 additions & 0 deletions src/registry/registry.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Controller, Post, Body, Param, Put, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { SmartContract, ContractStatus } from '../entities/contract.entity';
import { ContractVersion } from '../entities/version.entity';
import { VerificationService } from './verification.service';

@Controller('contracts')
export class RegistryController {
constructor(
@InjectRepository(SmartContract) private readonly contractRepo: Repository<SmartContract>,
@InjectRepository(ContractVersion) private readonly versionRepo: Repository<ContractVersion>,
private readonly verificationService: VerificationService,
) {}

@Post('register')
async registerContract(@Body() body: { contractId: string; name: string; version: string }) {
const contract = this.contractRepo.create({
contractId: body.contractId,
name: body.name,
status: ContractStatus.UNVERIFIED,
});
return this.contractRepo.save(contract);
}

@Post(':id/verify')
@UseInterceptors(FileInterceptor('wasm'))
async verifyVersion(
@Param('id') contractId: string,
@Body('versionStr') versionStr: string,
@UploadedFile() file: Express.Multer.File,
) {
const contract = await this.contractRepo.findOneByOrFail({ contractId });
const match = await this.verificationService.verifyContractBytecode(contractId, file.buffer);

const version = this.versionRepo.create({
versionStr,
wasmHash: this.verificationService.calculateHash(file.buffer),
isVerified: match,
contract,
});

await this.versionRepo.save(version);

if (match) {
contract.status = ContractStatus.VERIFIED;
await this.contractRepo.save(contract);
}

return { verified: match, hash: version.wasmHash };
}

@Put(':id/deprecate')
async deprecateContract(@Param('id') contractId: string, @Body('notice') notice: string) {
const contract = await this.contractRepo.findOneByOrFail({ contractId });
contract.status = ContractStatus.DEPRECATED;
contract.deprecationNotice = notice;
return this.contractRepo.save(contract);
}
}
41 changes: 41 additions & 0 deletions src/registry/verification.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Injectable, BadRequestException } from '@nestjs/common';
import { rpc, StrKey } from '@stellar/stellar-sdk';
import * as crypto from 'crypto';

@Injectable()
export class VerificationService {
private rpcInstance: rpc.Server;

constructor() {
// Falls back to Stellar testnet RPC endpoint
this.rpcInstance = new rpc.Server(process.env.STELLAR_RPC_URL || 'https://soroban-testnet.stellar.org');
}

/**
* Generates a deterministic SHA256 hash from raw file buffers
*/
calculateHash(buffer: Buffer): string {
return crypto.createHash('sha256').update(buffer).digest('hex');
}

/**
* Compares uploaded source compilation directly to live ledger states
*/
async verifyContractBytecode(contractId: string, uploadedWasm: Buffer): Promise<boolean> {
if (!StrKey.isValidContractId(contractId)) {
throw new BadRequestException('Invalid Soroban Contract ID format.');
}

try {
// Fetch structural ledger ledger entry directly from network RPC
const contractData = await this.rpcInstance.getContractWasmByContractId(contractId);

const onChainHash = Buffer.from(contractData.hash).toString('hex');
const uploadedHash = this.calculateHash(uploadedWasm);

return onChainHash === uploadedHash;
} catch (error) {
throw new BadRequestException(`Failed to cross-verify bytecode against network: ${error.message}`);
}
}
}
Loading