diff --git a/apps/api/src/apikeys/apikeys.controller.ts b/apps/api/src/apikeys/apikeys.controller.ts new file mode 100644 index 0000000..1eb151c --- /dev/null +++ b/apps/api/src/apikeys/apikeys.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Post, Request } from '@nestjs/common'; +import { ApikeysService } from './apikeys.service'; + +interface AuthenticatedRequest extends Request { + user?: { + id: string; + }; +} + +@Controller('apikeys') +export class ApikeysController { + constructor(private readonly apikeysService: ApikeysService) {} + + @Post() + async createApiKey(@Request() req: AuthenticatedRequest) { + // Assuming merchant ID is available from auth context, e.g., req.user?.id + const merchantId = req.user?.id || 'merchant-123'; + return this.apikeysService.generateApiKey(merchantId); + } +} diff --git a/apps/api/src/apikeys/apikeys.module.ts b/apps/api/src/apikeys/apikeys.module.ts new file mode 100644 index 0000000..77fdf2b --- /dev/null +++ b/apps/api/src/apikeys/apikeys.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ApikeysService } from './apikeys.service'; +import { ApikeysController } from './apikeys.controller'; + +@Module({ + controllers: [ApikeysController], + providers: [ApikeysService], + exports: [ApikeysService], +}) +export class ApikeysModule {} diff --git a/apps/api/src/apikeys/apikeys.service.ts b/apps/api/src/apikeys/apikeys.service.ts new file mode 100644 index 0000000..356d177 --- /dev/null +++ b/apps/api/src/apikeys/apikeys.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import * as crypto from 'crypto'; + +export interface ApiKeyRecord { + id: string; + merchantId: string; + key_hash: string; + createdAt: Date; +} + +@Injectable() +export class ApikeysService { + // TODO: Replace with actual database interaction + private readonly database: ApiKeyRecord[] = []; + + async generateApiKey(merchantId: string): Promise<{ plaintextKey: string }> { + // Generate 32-byte cryptographically random key + const rawKey = crypto.randomBytes(32).toString('hex'); + const plaintextKey = `sp_live_${rawKey}`; + + // Store only the SHA256 hash of the key in the database (key_hash) + const key_hash = crypto.createHash('sha256').update(plaintextKey).digest('hex'); + + const record: ApiKeyRecord = { + id: crypto.randomUUID(), + merchantId, + key_hash, + createdAt: new Date(), + }; + + this.database.push(record); + + // Response: Return the plaintext key only once to the merchant + return { plaintextKey }; + } +} diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 2fa7293..0a06c37 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -6,6 +6,7 @@ import { AppService } from './app.service'; import { HealthModule } from './health/health.module'; import { TreasuryModule } from './treasury/treasury.module'; import { AuthModule } from './auth/auth.module'; +import { ApikeysModule } from './apikeys/apikeys.module'; import { JwtAuthGuard } from './auth/guards/jwt-auth.guard'; import { ThrottlerRedisGuard } from './rate-limiter/guards/throttler-redis.guard'; import { WorkerModule } from './modules/worker/worker.module'; diff --git a/apps/frontend/src/app/checkout/page.tsx b/apps/frontend/src/app/checkout/page.tsx index 95cb110..820ffd6 100644 --- a/apps/frontend/src/app/checkout/page.tsx +++ b/apps/frontend/src/app/checkout/page.tsx @@ -511,7 +511,7 @@ export default function PaymentCheckout() { onClick={handleBankPayment} className="w-full bg-white hover:bg-zinc-100 text-black h-14 rounded-xl mt-6 text-base transition-all duration-200 shadow-lg shadow-white/10" > - I've Completed the Transfer + I've Completed the Transfer

@@ -611,7 +611,7 @@ export default function PaymentCheckout() { onClick={handleCryptoDetection} className="w-full bg-white hover:bg-zinc-100 text-black h-14 rounded-xl mt-6 text-base transition-all duration-200 shadow-lg shadow-white/10" > - I've Sent the Payment + I've Sent the Payment

diff --git a/apps/frontend/src/app/components/ui/ImageWithFallback.tsx b/apps/frontend/src/app/components/ui/ImageWithFallback.tsx index 0e26139..c3d7280 100644 --- a/apps/frontend/src/app/components/ui/ImageWithFallback.tsx +++ b/apps/frontend/src/app/components/ui/ImageWithFallback.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react' +import Image from 'next/image' const ERROR_IMG_SRC = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg==' @@ -10,7 +11,7 @@ export function ImageWithFallback(props: React.ImgHTMLAttributes ) : ( - {alt} + {alt} ) } diff --git a/apps/frontend/src/app/dashboard/page.tsx b/apps/frontend/src/app/dashboard/page.tsx index ef7b64e..034f05d 100644 --- a/apps/frontend/src/app/dashboard/page.tsx +++ b/apps/frontend/src/app/dashboard/page.tsx @@ -1,6 +1,7 @@ 'use client'; -import { motion } from 'motion/react'; +import { useMemo } from 'react'; +import { motion } from "motion/react"; import { TrendingUp, TrendingDown, @@ -49,6 +50,8 @@ const assets = [ { symbol: 'sETH', balance: '145.2341', usd: '232,251.75', change: '-1.2%' }, ]; +const barWidths = useMemo(() => assets.map(() => Math.random() * 40 + 60), []); + const transactions = [ { id: 'pay_9k2j3n4k5j6h', @@ -211,7 +214,7 @@ export default function OverviewPage() { diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index cc5c254..c4307fc 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -3,3 +3,4 @@ pub mod escrow; pub mod subscription; pub mod payment_intent; +pub mod treasury; diff --git a/contracts/src/treasury.rs b/contracts/src/treasury.rs new file mode 100644 index 0000000..e707af9 --- /dev/null +++ b/contracts/src/treasury.rs @@ -0,0 +1,146 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env}; + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DataKey { + Balance(Address), +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TreasuryBalance { + pub available_balance: i128, + pub reserved_balance: i128, +} + +#[contract] +pub struct TreasuryContract; + +#[contractimpl] +impl TreasuryContract { + pub fn mint(env: Env, admin: Address, to: Address, amount: i128) { + admin.require_auth(); + if amount <= 0 { + panic!("amount must be positive"); + } + + let key = DataKey::Balance(to.clone()); + let mut balance = env.storage().persistent().get::<_, TreasuryBalance>(&key).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance.available_balance = balance.available_balance.checked_add(amount).expect("overflow"); + + env.storage().persistent().set(&key, &balance); + } + + pub fn burn(env: Env, admin: Address, from: Address, amount: i128) { + admin.require_auth(); + if amount <= 0 { + panic!("amount must be positive"); + } + + let key = DataKey::Balance(from.clone()); + let mut balance = env.storage().persistent().get::<_, TreasuryBalance>(&key).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance.available_balance = balance.available_balance.checked_sub(amount).expect("underflow"); + + env.storage().persistent().set(&key, &balance); + } + + pub fn get_balance(env: Env, id: Address) -> TreasuryBalance { + let key = DataKey::Balance(id); + env.storage().persistent().get::<_, TreasuryBalance>(&key).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }) + } + + pub fn transfer(env: Env, admin: Address, from: Address, to: Address, amount: i128) { + admin.require_auth(); + if amount <= 0 { panic!("amount must be positive"); } + if from == to { return; } + + let key_from = DataKey::Balance(from.clone()); + let mut balance_from = env.storage().persistent().get::<_, TreasuryBalance>(&key_from).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance_from.available_balance = balance_from.available_balance.checked_sub(amount).expect("insufficient available balance"); + env.storage().persistent().set(&key_from, &balance_from); + + let key_to = DataKey::Balance(to.clone()); + let mut balance_to = env.storage().persistent().get::<_, TreasuryBalance>(&key_to).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance_to.available_balance = balance_to.available_balance.checked_add(amount).expect("overflow"); + env.storage().persistent().set(&key_to, &balance_to); + } + + pub fn reserve(env: Env, admin: Address, from: Address, amount: i128) { + admin.require_auth(); + if amount <= 0 { panic!("amount must be positive"); } + + let key = DataKey::Balance(from.clone()); + let mut balance = env.storage().persistent().get::<_, TreasuryBalance>(&key).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance.available_balance = balance.available_balance.checked_sub(amount).expect("insufficient available balance"); + balance.reserved_balance = balance.reserved_balance.checked_add(amount).expect("overflow"); + + env.storage().persistent().set(&key, &balance); + } + + pub fn release(env: Env, admin: Address, from: Address, amount: i128) { + admin.require_auth(); + if amount <= 0 { panic!("amount must be positive"); } + + let key = DataKey::Balance(from.clone()); + let mut balance = env.storage().persistent().get::<_, TreasuryBalance>(&key).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance.reserved_balance = balance.reserved_balance.checked_sub(amount).expect("insufficient reserved balance"); + balance.available_balance = balance.available_balance.checked_add(amount).expect("overflow"); + + env.storage().persistent().set(&key, &balance); + } + + pub fn transfer_reserved(env: Env, admin: Address, from: Address, to: Address, amount: i128) { + admin.require_auth(); + if amount <= 0 { panic!("amount must be positive"); } + + let key_from = DataKey::Balance(from.clone()); + let mut balance_from = env.storage().persistent().get::<_, TreasuryBalance>(&key_from).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance_from.reserved_balance = balance_from.reserved_balance.checked_sub(amount).expect("insufficient reserved balance"); + env.storage().persistent().set(&key_from, &balance_from); + + if from != to { + let key_to = DataKey::Balance(to.clone()); + let mut balance_to = env.storage().persistent().get::<_, TreasuryBalance>(&key_to).unwrap_or(TreasuryBalance { + available_balance: 0, + reserved_balance: 0, + }); + + balance_to.available_balance = balance_to.available_balance.checked_add(amount).expect("overflow"); + env.storage().persistent().set(&key_to, &balance_to); + } else { + balance_from.available_balance = balance_from.available_balance.checked_add(amount).expect("overflow"); + env.storage().persistent().set(&key_from, &balance_from); + } +}