Skip to content
Open
20 changes: 20 additions & 0 deletions apps/api/src/apikeys/apikeys.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 10 additions & 0 deletions apps/api/src/apikeys/apikeys.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
36 changes: 36 additions & 0 deletions apps/api/src/apikeys/apikeys.service.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
1 change: 1 addition & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/app/checkout/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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&apos;ve Completed the Transfer
I&#39;ve Completed the Transfer
</Button>

<p className="text-xs text-zinc-500 text-center pt-2">
Expand Down Expand Up @@ -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&apos;ve Sent the Payment
I&#39;ve Sent the Payment
</Button>

<p className="text-xs text-zinc-500 text-center pt-2">
Expand Down
5 changes: 3 additions & 2 deletions apps/frontend/src/app/components/ui/ImageWithFallback.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react'
import Image from 'next/image'

const ERROR_IMG_SRC =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
Expand All @@ -10,7 +11,7 @@ export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElemen
setDidError(true)
}

const { src, alt, style, className, ...rest } = props
const { src, alt, style, className, width = 88, height = 88, ...rest } = props

return didError ? (
<div
Expand All @@ -22,6 +23,6 @@ export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElemen
</div>
</div>
) : (
<img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
<Image src={src as string} alt={alt} className={className} style={style} width={width} height={height} {...rest} onError={handleError} />
)
}
7 changes: 5 additions & 2 deletions apps/frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { motion } from 'motion/react';
import { useMemo } from 'react';
import { motion } from "motion/react";
import {
TrendingUp,
TrendingDown,
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -211,7 +214,7 @@ export default function OverviewPage() {
<motion.div
className="h-full bg-gradient-to-r from-white/30 to-white/10"
initial={{ width: 0 }}
animate={{ width: `${[85, 72, 64][index % 3]}%` }}
animate={{ width: `${barWidths[index]}%` }}
transition={{ duration: 1, delay: 0.5 + index * 0.1 }}
/>
</div>
Expand Down
1 change: 1 addition & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
pub mod escrow;
pub mod subscription;
pub mod payment_intent;
pub mod treasury;
146 changes: 146 additions & 0 deletions contracts/src/treasury.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}