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
) : (
-
+
)
}
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);
+ }
+}