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
22 changes: 21 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -228,6 +241,13 @@ enum TransactionStatus {
COMPLETED
FAILED
CANCELLED

ESCROW_FUNDED
BLOCKCHAIN_SUBMITTED
CONFIRMING
CONFIRMED
DISPUTED
REFUNDED
}

enum TransactionType {
Expand Down
8 changes: 6 additions & 2 deletions src/blockchain/blockchain.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
32 changes: 32 additions & 0 deletions src/blockchain/blockchain.service.ts
Original file line number Diff line number Diff line change
@@ -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<SupportedChain, ethers.JsonRpcProvider>();

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,
};
}
}
5 changes: 5 additions & 0 deletions src/blockchain/enums/supported-chain.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum SupportedChain {
ETHEREUM = 'ethereum',
POLYGON = 'polygon',
BSC = 'bsc',
}
18 changes: 18 additions & 0 deletions src/blockchain/providers/provider.factory.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
84 changes: 84 additions & 0 deletions src/blockchain/wallet/wallet.service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
32 changes: 32 additions & 0 deletions src/transactions/transactions.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
89 changes: 89 additions & 0 deletions src/transactions/transactions.service.ts
Original file line number Diff line number Diff line change
@@ -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',
},
});
}
}
}
Loading