From d712bafb26688d74ff203b719ef2ddcf11176184 Mon Sep 17 00:00:00 2001 From: ayshadogo Date: Mon, 23 Feb 2026 07:51:56 +0100 Subject: [PATCH 1/2] Implement Blockchain Integration and Smart Contract Interaction --- prisma/schema.prisma | 22 ++++- .../dto/create-transaction.dto.ts | 0 src/transactions/dto/transaction-query.dto.ts | 0 .../dto/update-transaction.dto.ts | 0 .../enums/transaction-status.enum.ts | 0 .../enums/transaction-type.enum.ts | 0 src/transactions/transactions.controller.ts | 32 +++++++ src/transactions/transactions.service.ts | 89 +++++++++++++++++++ 8 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/transactions/dto/create-transaction.dto.ts create mode 100644 src/transactions/dto/transaction-query.dto.ts create mode 100644 src/transactions/dto/update-transaction.dto.ts create mode 100644 src/transactions/enums/transaction-status.enum.ts create mode 100644 src/transactions/enums/transaction-type.enum.ts create mode 100644 src/transactions/transactions.controller.ts create mode 100644 src/transactions/transactions.service.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 26f5f5d0..5223645c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -106,10 +106,23 @@ model Transaction { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - property Property? @relation(fields: [propertyId], references: [id], onDelete: SetNull) + buyerId String + sellerId String + currency String + blockchainHash String? + blockNumber Int? + confirmations Int @default(0) + escrowWallet String? + gasFee Decimal? + platformFee Decimal? + disputeReason String? + + property Property? @relation(fields: [propertyId], references: [id], onDelete: SetNull) recipient User? @relation("UserTransactions", fields: [toAddress], references: [walletAddress]) documents Document[] + @@index([buyerId]) + @@index([sellerId]) @@index([fromAddress]) @@index([toAddress]) @@index([status]) @@ -228,6 +241,13 @@ enum TransactionStatus { COMPLETED FAILED CANCELLED + + ESCROW_FUNDED + BLOCKCHAIN_SUBMITTED + CONFIRMING + CONFIRMED + DISPUTED + REFUNDED } enum TransactionType { diff --git a/src/transactions/dto/create-transaction.dto.ts b/src/transactions/dto/create-transaction.dto.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/transactions/dto/transaction-query.dto.ts b/src/transactions/dto/transaction-query.dto.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/transactions/dto/update-transaction.dto.ts b/src/transactions/dto/update-transaction.dto.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/transactions/enums/transaction-status.enum.ts b/src/transactions/enums/transaction-status.enum.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/transactions/enums/transaction-type.enum.ts b/src/transactions/enums/transaction-type.enum.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/transactions/transactions.controller.ts b/src/transactions/transactions.controller.ts new file mode 100644 index 00000000..5642c607 --- /dev/null +++ b/src/transactions/transactions.controller.ts @@ -0,0 +1,32 @@ +import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common"; +import { TransactionsService } from "./transactions.service"; + +@Controller('transactions') +export class TransactionsController { + constructor(private readonly service: TransactionsService) {} + + @Post() + create(@Body() dto: CreateTransactionDto) { + return this.service.createTransaction(dto); + } + + @Post(':id/escrow') + fundEscrow(@Param('id') id: string) { + return this.service.fundEscrow(id); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.service.getTransaction(id); + } + + @Get() + findAll(@Query() query: PaginationParams) { + return this.service.findAll(query); + } + + @Post(':id/dispute') + dispute(@Param('id') id: string, @Body() dto: DisputeDto) { + return this.service.raiseDispute(id, dto); + } +} \ No newline at end of file diff --git a/src/transactions/transactions.service.ts b/src/transactions/transactions.service.ts new file mode 100644 index 00000000..e7c0fa6b --- /dev/null +++ b/src/transactions/transactions.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from 'src/database'; +import { TransactionStatus } from 'src/models/transaction.entity'; + +@Injectable() +export class TransactionsService { + constructor( + private readonly prisma: PrismaService, + private readonly blockchainService: BlockchainService, + ) {} + + private calculateFees(amount: number) { + const platformFee = amount * 0.02; // 2% + const estimatedGas = this.blockchainService.estimateGas(); + + return { + platformFee, + estimatedGas, + }; + } + + private validateTransition( + current: TransactionStatus, + next: TransactionStatus, +) { + const allowedTransitions = { + PENDING: ['ESCROW_FUNDED', 'CANCELLED'], + ESCROW_FUNDED: ['BLOCKCHAIN_SUBMITTED'], + BLOCKCHAIN_SUBMITTED: ['CONFIRMING'], + CONFIRMING: ['CONFIRMED', 'FAILED'], + CONFIRMED: ['COMPLETED'], + }; + + if (!allowedTransitions[current]?.includes(next)) { + throw new BusinessException( + ERROR_CODES.INVALID_TRANSACTION_STATE, + `Invalid transition from ${current} to ${next}`, + ); + } +} + async createTransaction(dto: CreateTransactionDto) { + const fees = this.calculateFees(dto.amount); + + return this.prisma.transaction.create({ + data: { + ...dto, + status: 'PENDING', + platformFee: fees.platformFee, + gasFee: fees.estimatedGas, + }, + }); + } + + async fundEscrow(transactionId: string) { + const tx = await this.getTransaction(transactionId); + + const escrowWallet = await this.blockchainService.createEscrowWallet(); + + await this.prisma.transaction.update({ + where: { id: transactionId }, + data: { + escrowWallet, + status: 'ESCROW_FUNDED', + }, + }); + + return escrowWallet; +} + +async monitorBlockchain(transactionId: string) { + const tx = await this.getTransaction(transactionId); + + if (!tx.blockchainHash) return; + + const receipt = await this.blockchainService.getTransactionReceipt( + tx.blockchainHash, + ); + + if (receipt.confirmations >= 6) { + await this.prisma.transaction.update({ + where: { id: tx.id }, + data: { + confirmations: receipt.confirmations, + status: 'CONFIRMED', + }, + }); + } +} +} From c5dd02c52795caaa6a17870ed9c3e148bdf87b26 Mon Sep 17 00:00:00 2001 From: ayshadogo Date: Mon, 23 Feb 2026 08:02:03 +0100 Subject: [PATCH 2/2] Implement Complete Transaction Management System --- src/blockchain/blockchain.module.ts | 8 +- src/blockchain/blockchain.service.ts | 32 ++++++++ src/blockchain/enums/supported-chain.enum.ts | 5 ++ src/blockchain/providers/provider.factory.ts | 18 +++++ src/blockchain/wallet/wallet.service.ts | 84 ++++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/blockchain/blockchain.service.ts create mode 100644 src/blockchain/enums/supported-chain.enum.ts create mode 100644 src/blockchain/providers/provider.factory.ts create mode 100644 src/blockchain/wallet/wallet.service.ts diff --git a/src/blockchain/blockchain.module.ts b/src/blockchain/blockchain.module.ts index ba6960f9..a4e8b781 100644 --- a/src/blockchain/blockchain.module.ts +++ b/src/blockchain/blockchain.module.ts @@ -1,4 +1,8 @@ import { Module } from '@nestjs/common'; +import { BlockchainService } from './blockchain.service'; -@Module({}) -export class BlockchainModule {} +@Module({ + providers: [BlockchainService], + exports: [BlockchainService], +}) +export class BlockchainModule {} \ No newline at end of file diff --git a/src/blockchain/blockchain.service.ts b/src/blockchain/blockchain.service.ts new file mode 100644 index 00000000..ec982d08 --- /dev/null +++ b/src/blockchain/blockchain.service.ts @@ -0,0 +1,32 @@ +// blockchain.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { ethers } from 'ethers'; +import { ProviderFactory } from './providers/provider.factory'; +import { SupportedChain } from './enums/supported-chain.enum'; + +@Injectable() +export class BlockchainService { + private readonly logger = new Logger(BlockchainService.name); + private providers = new Map(); + + constructor() { + Object.values(SupportedChain).forEach(chain => { + this.providers.set(chain, ProviderFactory.create(chain)); + }); + } + + getProvider(chain: SupportedChain) { + return this.providers.get(chain); + } + + async getNetworkStatus(chain: SupportedChain) { + const provider = this.getProvider(chain); + const block = await provider.getBlockNumber(); + + return { + chain, + latestBlock: block, + healthy: true, + }; + } +} \ No newline at end of file diff --git a/src/blockchain/enums/supported-chain.enum.ts b/src/blockchain/enums/supported-chain.enum.ts new file mode 100644 index 00000000..698a01cc --- /dev/null +++ b/src/blockchain/enums/supported-chain.enum.ts @@ -0,0 +1,5 @@ +export enum SupportedChain { + ETHEREUM = 'ethereum', + POLYGON = 'polygon', + BSC = 'bsc', +} \ No newline at end of file diff --git a/src/blockchain/providers/provider.factory.ts b/src/blockchain/providers/provider.factory.ts new file mode 100644 index 00000000..bdf27300 --- /dev/null +++ b/src/blockchain/providers/provider.factory.ts @@ -0,0 +1,18 @@ +// providers/provider.factory.ts +import { JsonRpcProvider } from 'ethers'; +import { SupportedChain } from '../enums/supported-chain.enum'; + +export class ProviderFactory { + static create(chain: SupportedChain): JsonRpcProvider { + const rpcMap = { + ethereum: process.env.ETH_RPC, + polygon: process.env.POLYGON_RPC, + bsc: process.env.BSC_RPC, + }; + + const rpcUrl = rpcMap[chain]; + if (!rpcUrl) throw new Error(`RPC not configured for ${chain}`); + + return new JsonRpcProvider(rpcUrl); + } +} \ No newline at end of file diff --git a/src/blockchain/wallet/wallet.service.ts b/src/blockchain/wallet/wallet.service.ts new file mode 100644 index 00000000..5c47a3d9 --- /dev/null +++ b/src/blockchain/wallet/wallet.service.ts @@ -0,0 +1,84 @@ +import { ethers, Wallet } from 'ethers'; +import { SupportedChain } from '../enums/supported-chain.enum'; +import { ProviderFactory } from '../providers/provider.factory'; + +export class WalletService { + static getWallet(chain: SupportedChain) { + const provider = ProviderFactory.create(chain); + return new Wallet(process.env.PRIVATE_KEY, provider); + } + + async sendTransaction(chain: SupportedChain, to: string, value: string) { + const wallet = WalletService.getWallet(chain); + const provider = this.getProvider(chain); + + const feeData = await provider.getFeeData(); + + const tx = await wallet.sendTransaction({ + to, + value: ethers.parseEther(value), + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, + }); + + return tx; + } + + async getContract(chain: SupportedChain, address: string, abi: any) { + const wallet = WalletService.getWallet(chain); + return new ethers.Contract(address, abi, wallet); + } + + async listenToEvent( + chain: SupportedChain, + contractAddress: string, + abi: any, + eventName: string, + callback: (data: any) => void, + ) { + const provider = this.getProvider(chain); + const contract = new ethers.Contract(contractAddress, abi, provider); + + contract.on(eventName, (...args) => { + callback(args); + }); + } + + async syncBlocks(chain: SupportedChain) { + const provider = this.getProvider(chain); + const latest = await provider.getBlockNumber(); + + // store latest block in DB + // process missed events + } + async batchTransactions(chain: SupportedChain, txs: Array<{ to: string; value: string }>) { + const wallet = WalletService.getWallet(chain); + + return Promise.all( + txs.map(tx => + wallet.sendTransaction({ + to: tx.to, + value: ethers.parseEther(tx.value), + }), + ), + ); + } + + async checkHealth() { + const results = []; + + for (const chain of Object.values(SupportedChain)) { + try { + const status = await this.getNetworkStatus(chain); + results.push(status); + } catch { + results.push({ + chain, + healthy: false, + }); + } + } + + return results; + } +}