Skip to content
Open
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
95 changes: 95 additions & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@prisma/client": "^5.10.2",
"@stellar/stellar-sdk": "^11.3.0",
"bcrypt": "^5.1.1",
"redis": "^4.6.13",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
Expand All @@ -36,4 +37,4 @@
"ts-node-dev": "^2.0.0",
"typescript": "^5.3.3"
}
}
}
71 changes: 71 additions & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,32 @@ datasource db {
url = env("DATABASE_URL")
}

enum Currency {
NGN
USDC
XLM
}

enum TransactionStatus {
PENDING
SUCCESS
FAILED
}

enum TransactionType {
CREDIT
DEBIT
LOCK
RELEASE
SLASH
}

enum EscrowStatus {
LOCKED
RELEASED
SLASHED
}

model User {
id String @id @default(uuid())
email String @unique
Expand All @@ -15,6 +41,7 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
projects Project[]
wallet Wallet?
}

model Project {
Expand Down Expand Up @@ -51,3 +78,47 @@ model BlockchainEvent {
data Json
createdAt DateTime @default(now())
}

model Wallet {
id String @id @default(uuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
balanceNGN Decimal @default(0) @db.Decimal(65, 30)
balanceUSDC Decimal @default(0) @db.Decimal(65, 30)
balanceXLM Decimal @default(0) @db.Decimal(65, 30)
escrowNGN Decimal @default(0) @db.Decimal(65, 30)
escrowUSDC Decimal @default(0) @db.Decimal(65, 30)
escrowXLM Decimal @default(0) @db.Decimal(65, 30)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
transactions WalletTransaction[]
escrows Escrow[]
}

model WalletTransaction {
id String @id @default(uuid())
walletId String
wallet Wallet @relation(fields: [walletId], references: [id])
userId String
currency Currency
type TransactionType
amount Decimal @db.Decimal(65, 30)
status TransactionStatus @default(PENDING)
reference String?
metadata Json?
matchId String?
createdAt DateTime @default(now())
}

model Escrow {
id String @id @default(uuid())
matchId String
userId String
walletId String
wallet Wallet @relation(fields: [walletId], references: [id])
currency Currency
amount Decimal @db.Decimal(65, 30)
status EscrowStatus @default(LOCKED)
createdAt DateTime @default(now())
releasedAt DateTime?
}
2 changes: 2 additions & 0 deletions server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import morgan from 'morgan';
import dotenv from 'dotenv';
import routes from './routes/index';
import { errorHandler } from './middleware/error.middleware';
import webhookRoutes from './routes/webhooks.routes';

dotenv.config();

Expand All @@ -15,6 +16,7 @@ const port = process.env.PORT || 3000;
app.use(helmet());
app.use(cors());
app.use(morgan('dev'));
app.use('/api/webhooks', webhookRoutes);
app.use(express.json());

// Routes
Expand Down
29 changes: 29 additions & 0 deletions server/src/controllers/paystack.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import { Decimal } from '@prisma/client/runtime/library';
import { WalletService } from '../services/wallet.service';

const walletService = new WalletService();

export const paystackWebhook = async (req: Request, res: Response, next: NextFunction) => {
try {
const secret = process.env.PAYSTACK_SECRET_KEY || process.env.PAYSTACK_SECRET || '';
if (!secret) return res.status(500).send('misconfigured');
const signature = (req.headers['x-paystack-signature'] as string) || '';
const rawBody: Buffer = (req as any).body;
const computed = crypto.createHmac('sha512', secret).update(rawBody).digest('hex');
if (signature !== computed) return res.status(401).send('invalid signature');
const payload = JSON.parse(rawBody.toString('utf8'));
if (payload?.event === 'charge.success') {
const data = payload?.data;
const userId = data?.metadata?.userId as string | undefined;
if (userId) {
const amount = new Decimal(data.amount).div(100);
await walletService.addBalance(userId, 'NGN' as any, amount, data.reference, { source: 'paystack_webhook' });
}
}
res.json({ received: true });
} catch (e) {
next(e);
}
};
2 changes: 2 additions & 0 deletions server/src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Router } from 'express';
import authRoutes from './auth.routes';
import webhookRoutes from './webhooks.routes';

const router = Router();

router.use('/auth', authRoutes);
router.use('/webhooks', webhookRoutes);

// Placeholder for other routes
// router.use('/projects', projectRoutes);
Expand Down
8 changes: 8 additions & 0 deletions server/src/routes/webhooks.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from 'express';
import { paystackWebhook } from '../controllers/paystack.controller';

const router = express.Router();

router.post('/paystack', express.raw({ type: 'application/json' }), paystackWebhook);

export default router;
33 changes: 33 additions & 0 deletions server/src/services/paystack.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Decimal } from '@prisma/client/runtime/library';

export class PaystackService {
private readonly secret = process.env.PAYSTACK_SECRET_KEY || process.env.PAYSTACK_SECRET || '';
private readonly baseUrl = process.env.PAYSTACK_BASE_URL || 'https://api.paystack.co';

async verifyTransaction(reference: string) {
if (!this.secret) throw new Error('PAYSTACK_SECRET_KEY missing');
const res = await fetch(`${this.baseUrl}/transaction/verify/${encodeURIComponent(reference)}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${this.secret}`,
Accept: 'application/json',
},
});
if (!res.ok) throw new Error(`paystack verify failed ${res.status}`);
const json: any = await res.json();
const status = json?.data?.status;
const amountKobo = json?.data?.amount;
const amount = amountKobo != null ? new Decimal(amountKobo).div(100) : null;
return {
ok: status === 'success',
raw: json,
status,
amount,
currency: (json?.data?.currency as string | undefined) || 'NGN',
reference: json?.data?.reference as string | undefined,
customer: json?.data?.customer,
};
}
}

export default new PaystackService();
Loading
Loading