From e6100b05f6d5b0f7adef142fa3963de5aa5f58d6 Mon Sep 17 00:00:00 2001 From: Muhammad Auwal Date: Wed, 24 Jun 2026 20:53:03 +0100 Subject: [PATCH 1/2] feat(registry-service): implement contract registry microservice with bytecode verification (#355) --- .../contract-registry-service/Dockerfile | 14 +++++ src/entities/audit.entity.ts | 26 ++++++++ src/entities/contract.entity.ts | 39 ++++++++++++ src/entities/version.entity.ts | 26 ++++++++ src/registry/registry.controller.ts | 61 +++++++++++++++++++ src/registry/verification.service.ts | 41 +++++++++++++ 6 files changed, 207 insertions(+) create mode 100644 microservices/contract-registry-service/Dockerfile create mode 100644 src/entities/audit.entity.ts create mode 100644 src/entities/contract.entity.ts create mode 100644 src/entities/version.entity.ts create mode 100644 src/registry/registry.controller.ts create mode 100644 src/registry/verification.service.ts diff --git a/microservices/contract-registry-service/Dockerfile b/microservices/contract-registry-service/Dockerfile new file mode 100644 index 0000000..7f2b527 --- /dev/null +++ b/microservices/contract-registry-service/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/entities/audit.entity.ts b/src/entities/audit.entity.ts new file mode 100644 index 0000000..2a98851 --- /dev/null +++ b/src/entities/audit.entity.ts @@ -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; +} \ No newline at end of file diff --git a/src/entities/contract.entity.ts b/src/entities/contract.entity.ts new file mode 100644 index 0000000..e310cfe --- /dev/null +++ b/src/entities/contract.entity.ts @@ -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; +} \ No newline at end of file diff --git a/src/entities/version.entity.ts b/src/entities/version.entity.ts new file mode 100644 index 0000000..e551186 --- /dev/null +++ b/src/entities/version.entity.ts @@ -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; +} \ No newline at end of file diff --git a/src/registry/registry.controller.ts b/src/registry/registry.controller.ts new file mode 100644 index 0000000..c804bde --- /dev/null +++ b/src/registry/registry.controller.ts @@ -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, + @InjectRepository(ContractVersion) private readonly versionRepo: Repository, + 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); + } +} \ No newline at end of file diff --git a/src/registry/verification.service.ts b/src/registry/verification.service.ts new file mode 100644 index 0000000..d4432cc --- /dev/null +++ b/src/registry/verification.service.ts @@ -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 { + 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}`); + } + } +} \ No newline at end of file From 100335729f15900da77089c3ae0326cb289933d5 Mon Sep 17 00:00:00 2001 From: Muhammad Auwal Date: Wed, 24 Jun 2026 21:05:08 +0100 Subject: [PATCH 2/2] feat(personalization-service): initialize recommendation engine with tracking logic (#364) --- .../personalization-service/Dockerfile | 14 ++++ src/engine/personalization.controller.ts | 19 +++++ src/engine/personalization.service.ts | 83 +++++++++++++++++++ src/entities/preference.entity.ts | 24 ++++++ src/entities/variation.entity.ts | 25 ++++++ 5 files changed, 165 insertions(+) create mode 100644 microservices/personalization-service/Dockerfile create mode 100644 src/engine/personalization.controller.ts create mode 100644 src/engine/personalization.service.ts create mode 100644 src/entities/preference.entity.ts create mode 100644 src/entities/variation.entity.ts diff --git a/microservices/personalization-service/Dockerfile b/microservices/personalization-service/Dockerfile new file mode 100644 index 0000000..7f2b527 --- /dev/null +++ b/microservices/personalization-service/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/engine/personalization.controller.ts b/src/engine/personalization.controller.ts new file mode 100644 index 0000000..95ee87c --- /dev/null +++ b/src/engine/personalization.controller.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/engine/personalization.service.ts b/src/engine/personalization.service.ts new file mode 100644 index 0000000..718ac85 --- /dev/null +++ b/src/engine/personalization.service.ts @@ -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, + @InjectRepository(AbVariation) private readonly variantRepo: Repository, + ) {} + + /** + * Learns and increments category affinity scores over time based on user interactions + */ + async recordInteraction(userId: string, category: string, weight: number = 1.0): Promise { + 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 { + 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', + }; + } +} \ No newline at end of file diff --git a/src/entities/preference.entity.ts b/src/entities/preference.entity.ts new file mode 100644 index 0000000..ece5358 --- /dev/null +++ b/src/entities/preference.entity.ts @@ -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; + + // Calculated affinity scores used for ML matching loops + @Column({ type: 'jsonb', default: {} }) + categoryAffinities: Record; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; +} \ No newline at end of file diff --git a/src/entities/variation.entity.ts b/src/entities/variation.entity.ts new file mode 100644 index 0000000..22fc818 --- /dev/null +++ b/src/entities/variation.entity.ts @@ -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; // Specific layout structure updates + + @Column({ default: 0 }) + impressions: number; + + @Column({ default: 0 }) + conversions: number; + + @CreateDateColumn() + createdAt: Date; +} \ No newline at end of file