feat: add core and common modules with PostgreSQL integration

- Introduced CommonModule for shared services and configurations.
- Added CoreModule to manage database connections and configurations.
- Implemented PostgresService for PostgreSQL operations.
- Created configuration files for database and network provider.
- Integrated ethers.js for Ethereum interactions.
- Added validation pipes globally in the main application.
- Created DeployerModule with initial controller and service structure.
- Updated package.json with necessary dependencies for new features.
This commit is contained in:
Ricky Putra Pratama Tedjo 2025-08-04 15:40:09 +07:00
parent c6c947a402
commit 228b06f4c0
28 changed files with 5944 additions and 172 deletions

18
.gitignore vendored
View File

@ -54,3 +54,21 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
node_modules
.env
# Hardhat files
/cache
/artifacts
# TypeChain files
/typechain
/typechain-types
# solidity-coverage files
/coverage
/coverage.json
# Hardhat Ignition default folder for deployments against a local node
ignition/deployments/chain-31337

32
contracts/main.sol Normal file
View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract P2PTransferProject {
address public owner;
string public name;
modifier onlyOwner() {
require(msg.sender == owner, "Only owner");
_;
}
constructor(string memory _name) {
owner = msg.sender;
name = _name;
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
function setName(string memory _name) external onlyOwner {
name = _name;
}
function getName() external view returns (string memory) {
return name;
}
function getOwner() external view returns (address) {
return owner;
}
}

8
hardhat.config.ts Normal file
View File

@ -0,0 +1,8 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.28",
};
export default config;

5688
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,26 +23,36 @@
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nomicfoundation/hardhat-toolbox": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"ethers": "^6.15.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.18.0",
"@nestjs/class-validator": "^0.13.4",
"@nestjs/cli": "^11.0.0",
"@nestjs/config": "^4.0.2",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@openzeppelin/contracts": "^5.3.0",
"@swc/cli": "^0.6.0",
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.7",
"@types/pg": "^8.15.5",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"globals": "^16.0.0",
"hardhat": "^2.25.0",
"jest": "^29.7.0",
"pg": "^8.16.3",
"prettier": "^3.4.2",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",

View File

@ -1,7 +1,16 @@
import { Module } from '@nestjs/common';
import { ModulesModule } from './modules/module.module';
import { CoreModule } from './core/core.module';
import { CommonModule } from './common/common.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [],
imports: [
ConfigModule.forRoot({isGlobal:true}),
CommonModule,
CoreModule,
ModulesModule
],
controllers: [],
providers: [],
})

View File

@ -0,0 +1,15 @@
import { Global, Module } from "@nestjs/common";
import { ProviderService } from "./utils";
import { ConfigModule } from "@nestjs/config";
@Global()
@Module({
imports: [ConfigModule],
providers: [
ProviderService
],
exports: [
ProviderService
],
})
export class CommonModule {}

View File

@ -0,0 +1,6 @@
import { registerAs } from "@nestjs/config";
import { Database } from "../types";
export default registerAs<Database>('Database', () => ({
url: process.env.DATABASE_URL || null,
}));

View File

@ -0,0 +1,7 @@
import { registerAs } from "@nestjs/config";
import { NetworkProvider } from "../types";
export default registerAs<NetworkProvider>('NetworkProvider', ()=>({
providerUrl: process.env.NETWORK_PROVIDER || null,
walletKey: process.env.WALLET_KEY || null,
}));

View File

@ -0,0 +1,8 @@
export interface NetworkProvider {
providerUrl: string | null;
walletKey: string | null;
}
export interface Database{
url:string | null;
}

View File

@ -0,0 +1 @@
export * from './env.type';

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
export * from './provider.service';

View File

@ -0,0 +1,35 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ethers } from "ethers";
import * as P2PTransferProject from "./abis/P2PTransferProject.json";
@Injectable()
export class ProviderService {
constructor(private readonly config: ConfigService) {}
async getProviderAndWallet(): Promise<{ provider: ethers.JsonRpcProvider, wallet: ethers.Wallet }> {
try{
const providerUrl = this.config.get<string>('NetworkProvider.providerUrl');
const walletKey = this.config.get<string>('NetworkProvider.walletKey');
if (!providerUrl || !walletKey) {
throw new Error('Provider URL or Wallet Key is not configured');
}
const provider = new ethers.JsonRpcProvider(providerUrl);
const wallet = new ethers.Wallet(walletKey, provider);
return { provider, wallet };
} catch (error) {
throw new Error(`Failed to create provider or wallet: ${error.message}`);
}
}
async getAbiAndBytecode(){
return {
abi: P2PTransferProject.abi,
bytecode: P2PTransferProject.bytecode
};
}
}

25
src/core/core.module.ts Normal file
View File

@ -0,0 +1,25 @@
import { Global, Module } from "@nestjs/common";
import { PostgresService } from "./psql/postgres.service";
import { PostgresModule } from "./psql/postgres.module";
import { ConfigModule } from "@nestjs/config";
import providerConfig from "src/common/config/provider.config";
import dbConfig from "src/common/config/db.config";
@Global()
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [
dbConfig,
providerConfig
],
cache: true,
expandVariables: true,
}),
PostgresModule
],
exports: [PostgresModule],
})
export class CoreModule {}

View File

@ -0,0 +1,9 @@
import { Global, Module } from "@nestjs/common";
import { PostgresService } from "./postgres.service";
@Global()
@Module({
providers:[PostgresService],
exports: [PostgresService],
})
export class PostgresModule {}

View File

@ -0,0 +1,85 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Client } from 'pg'
@Injectable()
export class PostgresService {
private client: Client;
constructor(private readonly config: ConfigService) {
const connStr = this.config.get<string>('DATABASE_URL');
if (!connStr) {
throw new Error('DATABASE_URL is missing from configuration.');
}
this.client = new Client({
connectionString: connStr,
});
}
async onModuleInit() {
console.log('Connecting to Postgres...');
await this.connect(); // or initialize pool
}
async onModuleDestroy() {
await this.disconnect();
}
async connect() {
await this.client.connect();
}
async disconnect() {
await this.client.end();
}
async getAll<T = any>(table: string, filters?: Record<string, any>): Promise<T[]> {
let query = `SELECT * FROM ${table}`;
const values: any[] = [];
if (filters && Object.keys(filters).length > 0) {
const conditions = Object.entries(filters).map(([key, value], idx) => {
values.push(value);
// use LOWER() for Ethereum address comparison
return `LOWER(${key}) = LOWER($${idx + 1})`;
});
query += ` WHERE ` + conditions.join(' AND ');
}
const res = await this.client.query(query, values);
return res.rows;
}
async insert<T = any>(table: string, data: Record<string, any>): Promise<T> {
const keys = Object.keys(data)
const values = Object.values(data)
const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ')
const res = await this.client.query(
`INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders}) RETURNING *`,
values
)
return res.rows[0]
}
async update<T = any>(table: string, id: number, data: Record<string, any>): Promise<T> {
const keys = Object.keys(data)
const values = Object.values(data)
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(', ')
const res = await this.client.query(
`UPDATE ${table} SET ${setClause} WHERE id = $${keys.length + 1} RETURNING *`,
[...values, id]
)
return res.rows[0]
}
async delete<T = any>(table: string, id: number): Promise<T> {
const res = await this.client.query(
`DELETE FROM ${table} WHERE id = $1 RETURNING *`,
[id]
)
return res.rows[0]
}
}

View File

@ -1,8 +1,25 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist:true,
transform:true,
forbidNonWhitelisted:true
}),
);
//CORS configuration
// app.enableCors({
// origin: process.env.FRONTEND_URL,
// credentials: true,
// });
// app.setGlobalPrefix('api');
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

View File

@ -0,0 +1,6 @@
import { Controller } from "@nestjs/common";
@Controller()
export class DeployerController {
// Controller methods go here
}

View File

@ -0,0 +1 @@
export * from "./deployer.controller";

View File

@ -0,0 +1,9 @@
import { Module } from "@nestjs/common";
@Module({
imports: [],
controllers: [],
providers: [],
exports: [],
})
export class DeployerModule {}

View File

View File

@ -0,0 +1,6 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class DeployerService {
// Service methods go here
}

View File

@ -0,0 +1 @@
export * from './deployer.service'

View File

@ -0,0 +1,4 @@
export type Contract = {
contractaddress: string;
useraddress: string;
}

View File

@ -0,0 +1 @@
export * from './contract.type';

View File

@ -0,0 +1,6 @@
import { Module } from "@nestjs/common";
@Module({
imports: []
})
export class ModulesModule {}

View File

@ -6,6 +6,8 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"target": "ES2023",
"sourceMap": true,
"outDir": "./dist",
@ -14,8 +16,8 @@
"skipLibCheck": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": false,
"noImplicitAny": true,
"strictBindCallApply": false,
"noFallthroughCasesInSwitch": false
"noFallthroughCasesInSwitch": true
}
}