├── arch.png ├── app ├── favicon.ico ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── inventory │ │ └── route.ts │ ├── buy │ │ └── route.ts │ └── helius │ │ └── route.ts ├── types │ ├── next-auth.d.ts │ └── shamirs-secret-sharing.d.ts ├── components │ ├── Providers.tsx │ ├── Navbar.tsx │ └── ProductCard.tsx ├── layout.tsx ├── page.tsx └── globals.css ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20250711094419_add_partialkeytouser │ │ └── migration.sql │ └── 20250709232232_init │ │ └── migration.sql ├── index.ts └── schema.prisma ├── lib ├── utils.ts ├── addUsertoHelius.ts ├── authConfig.ts └── shamir-secret.ts ├── components.json ├── eslint.config.mjs ├── README.md ├── .gitignore ├── tsconfig.json ├── components └── ui │ ├── input.tsx │ ├── button.tsx │ ├── card.tsx │ └── select.tsx └── package.json /arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxn787/Payment-gateway/HEAD/arch.png -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alxn787/Payment-gateway/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import { authConfig } from "@/lib/authConfig"; 3 | import { AuthOptions } from "next-auth"; 4 | 5 | const handler = NextAuth(authConfig as AuthOptions); 6 | 7 | export { handler as GET, handler as POST }; -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prisma/migrations/20250711094419_add_partialkeytouser/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `PartialKey` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "PartialKey" DROP CONSTRAINT "PartialKey_userId_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "User" ADD COLUMN "partialKey" TEXT; 12 | 13 | -- DropTable 14 | DROP TABLE "PartialKey"; 15 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: ["app/generated/prisma/"], 16 | } 17 | ]; 18 | 19 | export default eslintConfig; 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypto Payment Gateway 2 | 3 | ## Architecture 4 | ![alt text](arch.png) 5 | 6 | This architecture is designed for secure, efficient, and scalable digital asset transactions, ready to power e-commerce, dApps, and beyond. 7 | 8 | Key features include: 9 | 10 | Key splitting across DB and Redis for enhanced security and resilience. 11 | 12 | Helius indexer with webhooks for automated address sweeping from split keypairs. 13 | 14 | Secure cold wallet integration for large fund protection. 15 | 16 | Shamir's secret for for highly secure transaction signing. 17 | -------------------------------------------------------------------------------- /prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@/app/generated/prisma"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient(); 5 | }; 6 | 7 | type PrismaClientSingleton = ReturnType; 8 | 9 | // eslint-disable-next-line 10 | const globalForPrisma = globalThis as unknown as { 11 | prisma: PrismaClientSingleton | undefined; 12 | }; 13 | 14 | const prisma = globalForPrisma.prisma ?? prismaClientSingleton(); 15 | 16 | export default prisma; 17 | 18 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; -------------------------------------------------------------------------------- /lib/addUsertoHelius.ts: -------------------------------------------------------------------------------- 1 | import { Helius} from 'helius-sdk'; // Replace with 'helius-sdk' in a production setting 2 | 3 | export async function addUsertoHelius(pubkey: string) { 4 | try{ 5 | 6 | const helius = new Helius(process.env.HELIUS_KEY as string); 7 | 8 | const webhookId = process.env.HELIUS_WEBHOOK_ID as string; 9 | 10 | const response = await helius.appendAddressesToWebhook(webhookId, [ 11 | pubkey, 12 | ]); 13 | return response; 14 | } catch (error) { 15 | console.error('Error adding user to Helius:', error); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | // next-auth.d.ts 2 | import { DefaultSession, DefaultUser } from "next-auth"; 3 | import { JWT as DefaultJWT } from "next-auth/jwt"; 4 | 5 | declare module "next-auth" { 6 | interface Session { 7 | user: DefaultSession["user"] & { 8 | uid?: string | null; 9 | pubKey?: string | null; 10 | }; 11 | } 12 | 13 | interface User extends DefaultUser { 14 | Pubkey?: string | null; 15 | } 16 | } 17 | 18 | declare module "next-auth/jwt" { 19 | interface JWT extends DefaultJWT { 20 | uid?: string | null; 21 | pubKey?: string | null; 22 | } 23 | } -------------------------------------------------------------------------------- /app/api/inventory/route.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '../../generated/prisma'; 2 | import { NextResponse } from 'next/server'; 3 | 4 | const prisma = new PrismaClient(); 5 | 6 | export async function GET() { 7 | try { 8 | const inv = await prisma.inventory.findMany(); 9 | 10 | return NextResponse.json({ message: "Inventory populated successfully!", data: inv }, { status: 200 }); 11 | } catch (error) { 12 | console.error("Error populating inventory:", error); 13 | return NextResponse.json({ message: "Failed to populate inventory.", error: error }, { status: 500 }); 14 | } finally { 15 | await prisma.$disconnect(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | /app/generated/prisma 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts","**/*.d.ts", ], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /app/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; 4 | import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; 5 | import '@solana/wallet-adapter-react-ui/styles.css'; 6 | 7 | 8 | 9 | export default function Providers({ children }: { children: React.ReactNode }) { 10 | return ( 11 | 12 | 13 | 14 | {children} 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /app/types/shamirs-secret-sharing.d.ts: -------------------------------------------------------------------------------- 1 | // src/types/shamirs-secret-sharing.d.ts 2 | 3 | declare module 'shamirs-secret-sharing' { 4 | /** 5 | * Splits a secret (Buffer) into 'shares' number of shares, 6 | * requiring 'threshold' of them to reconstruct the secret. 7 | * @param secret The secret value as a Buffer. 8 | * @param options An object with 'shares' (total number of shares) and 'threshold' (minimum required shares). 9 | * @returns An array of Buffer objects, each representing a share. 10 | */ 11 | function split(secret: Buffer, options: { shares: number; threshold: number }): Buffer[]; 12 | 13 | /** 14 | * Combines an array of shares (Buffers) to reconstruct the original secret. 15 | * @param shares An array of Buffer objects, each representing a share. 16 | * @returns The reconstructed secret as a Buffer. 17 | */ 18 | function combine(shares: Buffer[]): Buffer; 19 | } -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/api/buy/route.ts: -------------------------------------------------------------------------------- 1 | import { authConfig } from "@/lib/authConfig"; 2 | import prisma from "@/prisma"; 3 | import { getServerSession } from "next-auth"; 4 | 5 | export async function POST(req: Request) { 6 | const { productId , signature, productprice } = await req.json(); 7 | const session = await getServerSession(authConfig); 8 | if (!session?.user) { 9 | return new Response("Unauthorized", { 10 | status: 401, 11 | headers: { 12 | "Content-Type": "text/plain", 13 | }, 14 | }); 15 | } 16 | await prisma.inventory.update({ 17 | where: { 18 | id: productId, 19 | }, 20 | data: { 21 | stock: { 22 | decrement: 1, 23 | } 24 | }, 25 | }) 26 | const order = await prisma.order.create({ 27 | data: { 28 | userId: session.user.uid??"", 29 | amount: productprice, 30 | Signature: signature, 31 | paymentStatus: "confirmed", 32 | }, 33 | }); 34 | 35 | 36 | return Response.json({order: order}); 37 | 38 | } -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import Providers from "./components/Providers"; 5 | import { SessionProvider } from "next-auth/react"; 6 | 7 | 8 | const geistSans = Geist({ 9 | variable: "--font-geist-sans", 10 | subsets: ["latin"], 11 | }); 12 | 13 | const geistMono = Geist_Mono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | }); 17 | 18 | // export const metadata: Metadata = { 19 | // title: "Create Next App", 20 | // description: "Generated by create next app", 21 | // }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | return ( 29 | 30 | 33 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | output = "../app/generated/prisma" 10 | } 11 | 12 | datasource db { 13 | provider = "postgresql" 14 | url = env("DATABASE_URL") 15 | } 16 | 17 | model User { 18 | id String @id @default(uuid()) 19 | username String @unique 20 | Pubkey String 21 | name String? 22 | subId String 23 | ProfilePicture String? 24 | partialKey String? 25 | orders Order[] 26 | } 27 | 28 | model Order { 29 | id String @id @default(uuid()) 30 | userId String 31 | amount Float 32 | Signature String 33 | paymentStatus String 34 | user User @relation(fields: [userId], references: [id]) 35 | } 36 | 37 | model Inventory { 38 | id String @id @default(uuid()) 39 | name String 40 | price Float 41 | stock Int 42 | image String 43 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payment_gateway", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "postinstall": "prisma generate", 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^6.11.1", 14 | "@radix-ui/react-select": "^2.2.5", 15 | "@radix-ui/react-slot": "^1.2.3", 16 | "@solana/wallet-adapter-react": "^0.15.39", 17 | "@solana/wallet-adapter-react-ui": "^0.9.39", 18 | "@solana/web3.js": "^1.98.2", 19 | "@types/crypto-js": "^4.2.2", 20 | "@upstash/redis": "^1.35.1", 21 | "axios": "^1.10.0", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "crypto-js": "^4.2.0", 25 | "helius-sdk": "^1.5.1", 26 | "ioredis": "^5.6.1", 27 | "lucide-react": "^0.525.0", 28 | "next": "15.3.5", 29 | "next-auth": "^4.24.11", 30 | "react": "^19.0.0", 31 | "react-dom": "^19.0.0", 32 | "shamirs-secret-sharing": "^2.0.1", 33 | "tailwind-merge": "^3.3.1" 34 | }, 35 | "devDependencies": { 36 | "@eslint/eslintrc": "^3", 37 | "@tailwindcss/postcss": "^4", 38 | "@types/node": "^20", 39 | "@types/react": "^19", 40 | "@types/react-dom": "^19", 41 | "eslint": "^9", 42 | "eslint-config-next": "15.3.5", 43 | "prisma": "^6.11.1", 44 | "tailwindcss": "^4", 45 | "tw-animate-css": "^1.3.5", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /prisma/migrations/20250709232232_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "username" TEXT NOT NULL, 5 | "Pubkey" TEXT NOT NULL, 6 | "name" TEXT, 7 | "subId" TEXT NOT NULL, 8 | "ProfilePicture" TEXT, 9 | 10 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "Order" ( 15 | "id" TEXT NOT NULL, 16 | "userId" TEXT NOT NULL, 17 | "amount" DOUBLE PRECISION NOT NULL, 18 | "Signature" TEXT NOT NULL, 19 | "paymentStatus" TEXT NOT NULL, 20 | 21 | CONSTRAINT "Order_pkey" PRIMARY KEY ("id") 22 | ); 23 | 24 | -- CreateTable 25 | CREATE TABLE "PartialKey" ( 26 | "id" TEXT NOT NULL, 27 | "userId" TEXT NOT NULL, 28 | "key" TEXT NOT NULL, 29 | 30 | CONSTRAINT "PartialKey_pkey" PRIMARY KEY ("id") 31 | ); 32 | 33 | -- CreateTable 34 | CREATE TABLE "Inventory" ( 35 | "id" TEXT NOT NULL, 36 | "name" TEXT NOT NULL, 37 | "price" DOUBLE PRECISION NOT NULL, 38 | "stock" INTEGER NOT NULL, 39 | "image" TEXT NOT NULL, 40 | 41 | CONSTRAINT "Inventory_pkey" PRIMARY KEY ("id") 42 | ); 43 | 44 | -- CreateIndex 45 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 46 | 47 | -- AddForeignKey 48 | ALTER TABLE "Order" ADD CONSTRAINT "Order_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 49 | 50 | -- AddForeignKey 51 | ALTER TABLE "PartialKey" ADD CONSTRAINT "PartialKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 52 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | 'use client' 3 | import React, { useEffect, useState } from 'react'; 4 | import Navbar from './components/Navbar'; 5 | import ProductCard from './components/ProductCard'; 6 | import axios from 'axios'; 7 | 8 | type Inventory = { 9 | id: string; 10 | name: string; 11 | price: number; 12 | stock: number; 13 | image: string; 14 | } 15 | 16 | const Home = () => { 17 | const [inventory, setInventory] = useState([]); 18 | 19 | useEffect(() => { 20 | getInventory(); 21 | }, []); 22 | 23 | async function getInventory() { 24 | 25 | const response = await axios.get('/api/inventory/') 26 | const inventory: Inventory[] = response.data.data; 27 | setInventory(inventory); 28 | } 29 | 30 | return ( 31 |
32 | 33 | 34 |
35 |
36 |

37 | Welcome to{' '} 38 | 39 | SolanaStore 40 | 41 |

42 |

43 | Premium Gaming Equipment Powered by Solana 44 |

45 |
46 |
47 | 48 |
49 |
50 |

Featured Products

51 |

Discover our premium gaming equipment collection

52 |
53 | 54 |
55 | {inventory.map((product: Inventory) => ( 56 | 57 | ))} 58 |
59 | 60 | {inventory.length === 0 && ( 61 |
62 |

No products found matching your search.

63 |
64 | )} 65 |
66 | 67 |
68 |
69 |

70 | © 2024 SolanaStore. Powered by Solana blockchain technology. 71 |

72 |
73 |
74 |
75 | ); 76 | }; 77 | 78 | export default Home; 79 | -------------------------------------------------------------------------------- /lib/authConfig.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import prisma from "@/prisma"; 3 | import { Account, Profile, User, Session as NextAuthSession } from "next-auth"; // Import Session from next-auth 4 | import GoogleProvider from "next-auth/providers/google"; 5 | import { generateMPCWallet } from "./shamir-secret"; 6 | import { JWT } from "next-auth/jwt"; 7 | import { AuthOptions } from "next-auth"; 8 | import { addUsertoHelius } from "./addUsertoHelius"; 9 | 10 | 11 | export const authConfig: AuthOptions = { 12 | providers: [ 13 | GoogleProvider({ 14 | clientId: process.env.GOOGLE_CLIENT_ID ?? "", 15 | clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "", 16 | }), 17 | ], 18 | secret: process.env.NEXT_AUTH_SECRET ?? "s3cret", 19 | callbacks: { 20 | async session({ session, token }: { session: NextAuthSession; token: JWT }): Promise { 21 | if (session.user && token.uid && token.pubKey) { 22 | session.user.uid = token.uid; 23 | session.user.pubKey = token.pubKey; 24 | } 25 | return session; 26 | }, 27 | async jwt({ 28 | token, 29 | account, 30 | profile, 31 | }: { 32 | token: JWT; 33 | account: Account | null; 34 | profile?: Profile; 35 | }) { 36 | if (account?.providerAccountId) { 37 | const user = await prisma.user.findFirst({ 38 | where: { 39 | subId: account.providerAccountId, 40 | } 41 | }); 42 | if (user) { 43 | token.uid = user.id; 44 | token.pubKey = user.Pubkey; 45 | } 46 | } 47 | return token; 48 | }, 49 | async signIn({ 50 | user, 51 | account, 52 | profile, 53 | }: { 54 | user: User; 55 | account: Account | null; 56 | profile?: Profile; 57 | }) { 58 | if (account?.provider === "google") { 59 | const email = user.email; 60 | if (!email) { 61 | return false; 62 | } 63 | const existingUser = await prisma.user.findFirst({ 64 | where: { 65 | username: email, 66 | }, 67 | }); 68 | if (existingUser) { 69 | return true; 70 | } 71 | 72 | try { 73 | const mpcWallet = await generateMPCWallet(email); 74 | 75 | await addUsertoHelius(mpcWallet.publicKey); 76 | 77 | const createdUser = await prisma.user 78 | .create({ 79 | data: { 80 | username: email, 81 | name: profile?.name, 82 | subId: account?.providerAccountId, 83 | Pubkey: mpcWallet.publicKey, 84 | ProfilePicture: profile?.image, 85 | partialKey: mpcWallet.encryptedShare1, 86 | }, 87 | }) 88 | return true; 89 | } catch (error) { 90 | console.error("Failed to create MPC wallet:", error); 91 | return false; 92 | } 93 | } 94 | return false; 95 | }, 96 | }, 97 | }; -------------------------------------------------------------------------------- /app/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { useState, useEffect } from 'react'; 3 | import { ShoppingCart, LogIn, LogOut } from 'lucide-react'; 4 | import { Button } from '@/components/ui/button'; 5 | import { signIn, signOut, useSession } from 'next-auth/react'; 6 | import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; 7 | 8 | 9 | const Navbar = ( ) => { 10 | const [isScrolled, setIsScrolled] = useState(false); 11 | const session = useSession(); 12 | const image = session?.data?.user?.image || "https://imgs.search.brave.com/B1aaBgz_pXUkBvbO88vfuUOU0_ZwfLeMlQuyPZ9tzR8/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly93YWxs/cGFwZXJzLmNvbS9p/bWFnZXMvaGQvYmFj/ay12aWV3LWdva3Ut/dWx0cmEtaW5zdGlu/Y3QtdTE2ZHBqM3k2/Mmd1eTJwcy5qcGc"; 13 | 14 | useEffect(() => { 15 | const handleScroll = () => { 16 | setIsScrolled(window.scrollY > 0); 17 | }; 18 | 19 | window.addEventListener('scroll', handleScroll); 20 | return () => window.removeEventListener('scroll', handleScroll); 21 | }, []); 22 | 23 | return ( 24 | 72 | ); 73 | }; 74 | 75 | export default Navbar; -------------------------------------------------------------------------------- /app/api/helius/route.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import prisma from '@/prisma'; 3 | import { signTransactionMPC } from '../../../lib/shamir-secret'; 4 | import { NextResponse } from 'next/server'; 5 | import { Connection, PublicKey, SystemProgram, Transaction} from '@solana/web3.js'; 6 | 7 | 8 | export async function POST(Request: Request) { 9 | 10 | const SOLANA_DEVNET_RPC_URL = "https://api.devnet.solana.com"; 11 | const content = await Request.json(); 12 | const tx = content[0]; 13 | if (tx.meta.err != null) { 14 | throw new Error("Transaction didnt go through"); 15 | } 16 | 17 | if (!process.env.HOTWALLET_PUBKEY) { 18 | console.error('Environment variable HOTWALLET_PUBKEY is not set.'); 19 | return NextResponse.json( 20 | { error: 'Server configuration error: Hot wallet public key is not defined.' }, 21 | { status: 500 } 22 | ); 23 | } 24 | try{ 25 | const recipient = tx.transaction.message.accountKeys[1]; 26 | 27 | const postBalance = tx.meta.postBalances[1]; 28 | const databaseShare = await prisma.user.findFirst({ 29 | where: { 30 | Pubkey: recipient 31 | }, 32 | select:{ 33 | partialKey: true, 34 | username:true, 35 | Pubkey: true, 36 | } 37 | }); 38 | 39 | if(!databaseShare){ 40 | return NextResponse.json( 41 | { message: 'payment going out of the wallet'}, 42 | { status: 200 } 43 | ); 44 | } 45 | 46 | const connection = new Connection(SOLANA_DEVNET_RPC_URL); 47 | 48 | const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); 49 | 50 | const transaction = new Transaction 51 | transaction.recentBlockhash = blockhash; 52 | transaction.lastValidBlockHeight = lastValidBlockHeight; 53 | transaction.feePayer = new PublicKey(recipient); 54 | 55 | const fromPubkey = new PublicKey(recipient); 56 | const toPubkey = new PublicKey(process.env.HOTWALLET_PUBKEY as string); 57 | 58 | const amountToTransferLamports = postBalance - 100000000; 59 | 60 | transaction.add( 61 | SystemProgram.transfer({ 62 | fromPubkey: fromPubkey, 63 | toPubkey: toPubkey, 64 | lamports: amountToTransferLamports, 65 | }) 66 | ); 67 | 68 | if(databaseShare.partialKey == null){ 69 | throw new Error("Database share not found"); 70 | } 71 | 72 | const signedTransaction = await signTransactionMPC(databaseShare.username, transaction, databaseShare.partialKey); 73 | 74 | const serializedTransaction = signedTransaction.serialize(); 75 | 76 | const txid = await connection.sendRawTransaction(serializedTransaction, { 77 | skipPreflight: false, 78 | }); 79 | 80 | return NextResponse.json( 81 | { message: 'Webhook payload processed', txid: txid }, 82 | { status: 200 } 83 | ); 84 | 85 | }catch(error){ 86 | console.error("Error processing Helius webhook:", error); 87 | return NextResponse.json( 88 | { error: 'Failed to process webhook payload', details: error || 'An unknown error occurred' }, 89 | { status: 500 } 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "tw-animate-css"; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme inline { 7 | --color-background: var(--background); 8 | --color-foreground: var(--foreground); 9 | --font-sans: var(--font-geist-sans); 10 | --font-mono: var(--font-geist-mono); 11 | --color-sidebar-ring: var(--sidebar-ring); 12 | --color-sidebar-border: var(--sidebar-border); 13 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 14 | --color-sidebar-accent: var(--sidebar-accent); 15 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 16 | --color-sidebar-primary: var(--sidebar-primary); 17 | --color-sidebar-foreground: var(--sidebar-foreground); 18 | --color-sidebar: var(--sidebar); 19 | --color-chart-5: var(--chart-5); 20 | --color-chart-4: var(--chart-4); 21 | --color-chart-3: var(--chart-3); 22 | --color-chart-2: var(--chart-2); 23 | --color-chart-1: var(--chart-1); 24 | --color-ring: var(--ring); 25 | --color-input: var(--input); 26 | --color-border: var(--border); 27 | --color-destructive: var(--destructive); 28 | --color-accent-foreground: var(--accent-foreground); 29 | --color-accent: var(--accent); 30 | --color-muted-foreground: var(--muted-foreground); 31 | --color-muted: var(--muted); 32 | --color-secondary-foreground: var(--secondary-foreground); 33 | --color-secondary: var(--secondary); 34 | --color-primary-foreground: var(--primary-foreground); 35 | --color-primary: var(--primary); 36 | --color-popover-foreground: var(--popover-foreground); 37 | --color-popover: var(--popover); 38 | --color-card-foreground: var(--card-foreground); 39 | --color-card: var(--card); 40 | --radius-sm: calc(var(--radius) - 4px); 41 | --radius-md: calc(var(--radius) - 2px); 42 | --radius-lg: var(--radius); 43 | --radius-xl: calc(var(--radius) + 4px); 44 | } 45 | 46 | :root { 47 | --radius: 0.625rem; 48 | --background: oklch(1 0 0); 49 | --foreground: oklch(0.145 0 0); 50 | --card: oklch(1 0 0); 51 | --card-foreground: oklch(0.145 0 0); 52 | --popover: oklch(1 0 0); 53 | --popover-foreground: oklch(0.145 0 0); 54 | --primary: oklch(0.205 0 0); 55 | --primary-foreground: oklch(0.985 0 0); 56 | --secondary: oklch(0.97 0 0); 57 | --secondary-foreground: oklch(0.205 0 0); 58 | --muted: oklch(0.97 0 0); 59 | --muted-foreground: oklch(0.556 0 0); 60 | --accent: oklch(0.97 0 0); 61 | --accent-foreground: oklch(0.205 0 0); 62 | --destructive: oklch(0.577 0.245 27.325); 63 | --border: oklch(0.922 0 0); 64 | --input: oklch(0.922 0 0); 65 | --ring: oklch(0.708 0 0); 66 | --chart-1: oklch(0.646 0.222 41.116); 67 | --chart-2: oklch(0.6 0.118 184.704); 68 | --chart-3: oklch(0.398 0.07 227.392); 69 | --chart-4: oklch(0.828 0.189 84.429); 70 | --chart-5: oklch(0.769 0.188 70.08); 71 | --sidebar: oklch(0.985 0 0); 72 | --sidebar-foreground: oklch(0.145 0 0); 73 | --sidebar-primary: oklch(0.205 0 0); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.97 0 0); 76 | --sidebar-accent-foreground: oklch(0.205 0 0); 77 | --sidebar-border: oklch(0.922 0 0); 78 | --sidebar-ring: oklch(0.708 0 0); 79 | } 80 | 81 | .dark { 82 | --background: oklch(0.145 0 0); 83 | --foreground: oklch(0.985 0 0); 84 | --card: oklch(0.205 0 0); 85 | --card-foreground: oklch(0.985 0 0); 86 | --popover: oklch(0.205 0 0); 87 | --popover-foreground: oklch(0.985 0 0); 88 | --primary: oklch(0.922 0 0); 89 | --primary-foreground: oklch(0.205 0 0); 90 | --secondary: oklch(0.269 0 0); 91 | --secondary-foreground: oklch(0.985 0 0); 92 | --muted: oklch(0.269 0 0); 93 | --muted-foreground: oklch(0.708 0 0); 94 | --accent: oklch(0.269 0 0); 95 | --accent-foreground: oklch(0.985 0 0); 96 | --destructive: oklch(0.704 0.191 22.216); 97 | --border: oklch(1 0 0 / 10%); 98 | --input: oklch(1 0 0 / 15%); 99 | --ring: oklch(0.556 0 0); 100 | --chart-1: oklch(0.488 0.243 264.376); 101 | --chart-2: oklch(0.696 0.17 162.48); 102 | --chart-3: oklch(0.769 0.188 70.08); 103 | --chart-4: oklch(0.627 0.265 303.9); 104 | --chart-5: oklch(0.645 0.246 16.439); 105 | --sidebar: oklch(0.205 0 0); 106 | --sidebar-foreground: oklch(0.985 0 0); 107 | --sidebar-primary: oklch(0.488 0.243 264.376); 108 | --sidebar-primary-foreground: oklch(0.985 0 0); 109 | --sidebar-accent: oklch(0.269 0 0); 110 | --sidebar-accent-foreground: oklch(0.985 0 0); 111 | --sidebar-border: oklch(1 0 0 / 10%); 112 | --sidebar-ring: oklch(0.556 0 0); 113 | } 114 | 115 | @layer base { 116 | * { 117 | @apply border-border outline-ring/50; 118 | } 119 | body { 120 | @apply bg-background text-foreground; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/components/ProductCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ShoppingCart, Star } from 'lucide-react'; 4 | import { Button } from '@/components/ui/button'; 5 | import { Card, CardContent } from '@/components/ui/card'; 6 | import axios from 'axios'; 7 | import { useState, useCallback } from 'react'; 8 | import { Connection, PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; 9 | import { useSession } from 'next-auth/react'; 10 | import { useWallet } from '@solana/wallet-adapter-react'; 11 | 12 | interface Product { 13 | id: string; 14 | name: string; 15 | price: number; 16 | stock: number; 17 | image: string; 18 | } 19 | 20 | interface ProductCardProps { 21 | product: Product; 22 | } 23 | 24 | const ProductCard = ({ product }: ProductCardProps) => { 25 | const [isBuying, setIsBuying] = useState(false); 26 | const session = useSession(); 27 | const { wallet, connected }= useWallet(); 28 | 29 | 30 | const handleBuy = useCallback(async (productPrice: number) => { 31 | if(!session.data?.user?.pubKey) { 32 | return; 33 | } 34 | if (!connected || !wallet?.adapter.publicKey) { 35 | return; 36 | } 37 | setIsBuying(true); 38 | try { 39 | const connection = new Connection("https://api.devnet.solana.com"); 40 | const transaction = new Transaction().add( 41 | SystemProgram.transfer({ 42 | fromPubkey: wallet.adapter.publicKey, 43 | toPubkey: new PublicKey(session.data?.user?.pubKey??""), 44 | lamports:Number(productPrice) * 1000000000 45 | }) 46 | ); 47 | 48 | const latestBlockHash = await connection.getLatestBlockhash(); 49 | transaction.recentBlockhash = latestBlockHash.blockhash; 50 | transaction.lastValidBlockHeight = latestBlockHash.lastValidBlockHeight; 51 | transaction.feePayer = wallet.adapter.publicKey; 52 | const tx = await wallet.adapter.sendTransaction(transaction, connection,{ 53 | preflightCommitment: "confirmed", 54 | }); 55 | const confirmation = await connection.confirmTransaction({ 56 | signature: tx, 57 | blockhash: latestBlockHash.blockhash, 58 | lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, 59 | }, "confirmed"); 60 | if(confirmation.value.err == null){ 61 | alert("Transaction successful"); 62 | axios.post('/api/buy/', { 63 | productId: product.id, 64 | signature: confirmation.context.slot.toString(), 65 | productprice: productPrice 66 | }); 67 | } 68 | 69 | } catch (error) { 70 | console.error('Error buying product:', error); 71 | } finally { 72 | setIsBuying(false); 73 | } 74 | }, [product.id]); 75 | 76 | return ( 77 | 78 |
79 | {product.name} 84 |
85 | 86 | 87 |
88 |
89 | {[1, 2, 3, 4, 5].map((star) => ( 90 | 91 | ))} 92 |
93 | (50 reviews) 94 |
95 | 96 |

97 | {product.name} 98 |

99 | 100 |
101 | 102 | ${product.price.toFixed(5)} 103 | 104 | 105 | Stock: {product.stock} 106 | 107 |
108 | 109 | 123 |
124 |
125 | ); 126 | }; 127 | 128 | export default ProductCard; -------------------------------------------------------------------------------- /components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Select({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function SelectGroup({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function SelectValue({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function SelectTrigger({ 28 | className, 29 | size = "default", 30 | children, 31 | ...props 32 | }: React.ComponentProps & { 33 | size?: "sm" | "default" 34 | }) { 35 | return ( 36 | 45 | {children} 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | function SelectContent({ 54 | className, 55 | children, 56 | position = "popper", 57 | ...props 58 | }: React.ComponentProps) { 59 | return ( 60 | 61 | 72 | 73 | 80 | {children} 81 | 82 | 83 | 84 | 85 | ) 86 | } 87 | 88 | function SelectLabel({ 89 | className, 90 | ...props 91 | }: React.ComponentProps) { 92 | return ( 93 | 98 | ) 99 | } 100 | 101 | function SelectItem({ 102 | className, 103 | children, 104 | ...props 105 | }: React.ComponentProps) { 106 | return ( 107 | 115 | 116 | 117 | 118 | 119 | 120 | {children} 121 | 122 | ) 123 | } 124 | 125 | function SelectSeparator({ 126 | className, 127 | ...props 128 | }: React.ComponentProps) { 129 | return ( 130 | 135 | ) 136 | } 137 | 138 | function SelectScrollUpButton({ 139 | className, 140 | ...props 141 | }: React.ComponentProps) { 142 | return ( 143 | 151 | 152 | 153 | ) 154 | } 155 | 156 | function SelectScrollDownButton({ 157 | className, 158 | ...props 159 | }: React.ComponentProps) { 160 | return ( 161 | 169 | 170 | 171 | ) 172 | } 173 | 174 | export { 175 | Select, 176 | SelectContent, 177 | SelectGroup, 178 | SelectItem, 179 | SelectLabel, 180 | SelectScrollDownButton, 181 | SelectScrollUpButton, 182 | SelectSeparator, 183 | SelectTrigger, 184 | SelectValue, 185 | } 186 | -------------------------------------------------------------------------------- /lib/shamir-secret.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Keypair, PublicKey, Transaction } from "@solana/web3.js"; 3 | import * as sss from "shamirs-secret-sharing"; 4 | import CryptoJS from "crypto-js"; 5 | import { Redis } from '@upstash/redis' 6 | 7 | 8 | const MPC_CONFIG = { 9 | THRESHOLD: 3, 10 | TOTAL_SHARES: 3, 11 | ENCRYPTION_KEY: 12 | process.env.MPC_ENCRYPTION_KEY || "default-key-change-in-production", 13 | REDIS_URL_1: process.env.REDIS_URL_1 , 14 | REDIS_URL_2: process.env.REDIS_URL_2 , 15 | REDIS_TOKEN_1: process.env.REDIS_TOKEN_1 , 16 | REDIS_TOKEN_2: process.env.REDIS_TOKEN_2 , 17 | SHARE_TTL_DAYS: 30, 18 | ROTATION_WARNING_DAYS: 7, 19 | }; 20 | 21 | const redis1 = new Redis({url:MPC_CONFIG.REDIS_URL_1, token:MPC_CONFIG.REDIS_TOKEN_1}); 22 | const redis2 = new Redis({url:MPC_CONFIG.REDIS_URL_2, token:MPC_CONFIG.REDIS_TOKEN_2}); 23 | 24 | export interface KeyShare { 25 | shareIndex: number; 26 | shareData: string; 27 | publicKey: string; 28 | expiresAt?: Date; 29 | } 30 | 31 | export interface MPCWallet { 32 | publicKey: string; 33 | encryptedShare1: string; // PostgreSQL database storage 34 | encryptedShare2: string; // Redis 1 storage 35 | encryptedShare3?: string; // Redis 2 storage 36 | shareIndices: number[]; 37 | } 38 | 39 | function encryptData(data: string, key: string): string { 40 | return CryptoJS.AES.encrypt(data, key).toString(); 41 | } 42 | 43 | function decryptData(encryptedData: string, key: string): string { 44 | const bytes = CryptoJS.AES.decrypt(encryptedData, key); 45 | return bytes.toString(CryptoJS.enc.Utf8); 46 | } 47 | 48 | export async function checkShareExpiration(userId: string): Promise<{ 49 | needsRotation: boolean; 50 | daysUntilExpiry: number; 51 | missingShares: string[]; 52 | }> { 53 | const redisKey2 = `mpc:share:${userId}:2`; 54 | const redisKey3 = `mpc:share:${userId}:3`; 55 | 56 | const missingShares: string[] = []; 57 | let minTTL = Infinity; 58 | 59 | try { 60 | const ttl2 = await redis1.ttl(redisKey2); 61 | if (ttl2 === -2) { 62 | missingShares.push("share2"); 63 | } else if (ttl2 > 0) { 64 | minTTL = Math.min(minTTL, ttl2); 65 | } 66 | 67 | const ttl3 = await redis2.ttl(redisKey3); 68 | if (ttl3 === -2) { 69 | missingShares.push("share3"); 70 | } else if (ttl3 > 0) { 71 | minTTL = Math.min(minTTL, ttl3); 72 | } 73 | 74 | const daysUntilExpiry = 75 | minTTL === Infinity ? 0 : Math.floor(minTTL / (24 * 60 * 60)); 76 | const needsRotation = 77 | daysUntilExpiry <= MPC_CONFIG.ROTATION_WARNING_DAYS || 78 | missingShares.length > 0; 79 | 80 | return { 81 | needsRotation, 82 | daysUntilExpiry, 83 | missingShares, 84 | }; 85 | } catch (error) { 86 | console.error("Error checking share expiration:", error); 87 | return { 88 | needsRotation: true, 89 | daysUntilExpiry: 0, 90 | missingShares: ["share2", "share3"], 91 | }; 92 | } 93 | } 94 | 95 | export async function recoverMissingShares( 96 | userId: string, 97 | databaseShare: string 98 | ): Promise<{ share2: string; share3: string }> { 99 | 100 | try { 101 | const redisKey2 = `mpc:share:${userId}:2`; 102 | const redisKey3 = `mpc:share:${userId}:3`; 103 | 104 | const availableShares: Buffer[] = []; 105 | 106 | const decryptedShare1 = decryptData( 107 | databaseShare, 108 | MPC_CONFIG.ENCRYPTION_KEY 109 | ); 110 | availableShares.push(Buffer.from(decryptedShare1, "hex")); 111 | 112 | try { 113 | const encryptedShare2 = await redis1.get(redisKey2); 114 | if (encryptedShare2) { 115 | const decryptedShare2 = decryptData( 116 | encryptedShare2, 117 | MPC_CONFIG.ENCRYPTION_KEY 118 | ); 119 | availableShares.push(Buffer.from(decryptedShare2, "hex")); 120 | } 121 | } catch (error) { 122 | console.warn("Could not retrieve share 2:", error); 123 | } 124 | 125 | try { 126 | const encryptedShare3 = await redis2.get(redisKey3); 127 | if (encryptedShare3) { 128 | const decryptedShare3 = decryptData( 129 | encryptedShare3, 130 | MPC_CONFIG.ENCRYPTION_KEY 131 | ); 132 | availableShares.push(Buffer.from(decryptedShare3, "hex")); 133 | } 134 | } catch (error) { 135 | console.warn("Could not retrieve share 3:", error); 136 | } 137 | 138 | if (availableShares.length < 3) { 139 | throw new Error( 140 | `Cannot recover - only ${availableShares.length} shares available, need 3` 141 | ); 142 | } 143 | 144 | const privateKey = sss.combine(availableShares); 145 | 146 | const newShares = sss.split(privateKey, { 147 | shares: MPC_CONFIG.TOTAL_SHARES, 148 | threshold: MPC_CONFIG.THRESHOLD, 149 | }); 150 | 151 | const encryptedShare2 = encryptData( 152 | newShares[1].toString("hex"), 153 | MPC_CONFIG.ENCRYPTION_KEY 154 | ); 155 | const encryptedShare3 = encryptData( 156 | newShares[2].toString("hex"), 157 | MPC_CONFIG.ENCRYPTION_KEY 158 | ); 159 | 160 | privateKey.fill(0); 161 | 162 | return { 163 | share2: encryptedShare2, 164 | share3: encryptedShare3, 165 | }; 166 | } catch (error) { 167 | console.error("Failed to recover missing shares:", error); 168 | throw new Error( 169 | "Share recovery failed - wallet may be permanently inaccessible" 170 | ); 171 | } 172 | } 173 | 174 | export async function ensureShareAvailability( 175 | userId: string, 176 | databaseShare: string 177 | ): Promise { 178 | try { 179 | const status = await checkShareExpiration(userId); 180 | 181 | if (!status.needsRotation && status.missingShares.length === 0) { 182 | return true; 183 | } 184 | 185 | 186 | const recoveredShares = await recoverMissingShares(userId, databaseShare); 187 | 188 | const ttlSeconds = MPC_CONFIG.SHARE_TTL_DAYS * 24 * 60 * 60; 189 | 190 | const redisKey2 = `mpc:share:${userId}:2`; 191 | const redisKey3 = `mpc:share:${userId}:3`; 192 | 193 | await redis1.setex(redisKey2, ttlSeconds, recoveredShares.share2); 194 | await redis2.setex(redisKey3, ttlSeconds, recoveredShares.share3); 195 | 196 | return true; 197 | } catch (error) { 198 | console.error( 199 | `Failed to ensure share availability for user ${userId}:`, 200 | error 201 | ); 202 | return false; 203 | } 204 | } 205 | 206 | export async function generateMPCWallet(userId: string): Promise { 207 | const keypair = Keypair.generate(); 208 | const privateKeyBytes = keypair.secretKey; 209 | const publicKey = keypair.publicKey.toBase58(); 210 | 211 | const shares = sss.split(Buffer.from(privateKeyBytes), { 212 | shares: MPC_CONFIG.TOTAL_SHARES, 213 | threshold: MPC_CONFIG.THRESHOLD, 214 | }); 215 | 216 | const encryptedShares = shares.map((share, index) => ({ 217 | shareIndex: index + 1, 218 | shareData: encryptData(share.toString("hex"), MPC_CONFIG.ENCRYPTION_KEY), 219 | publicKey, 220 | })); 221 | 222 | const ttlSeconds = MPC_CONFIG.SHARE_TTL_DAYS * 24 * 60 * 60; 223 | const redisKey2 = `mpc:share:${userId}:2`; 224 | await redis1.setex(redisKey2, ttlSeconds, encryptedShares[1].shareData); 225 | 226 | const redisKey3 = `mpc:share:${userId}:3`; 227 | await redis2.setex(redisKey3, ttlSeconds, encryptedShares[2].shareData); 228 | 229 | return { 230 | publicKey, 231 | encryptedShare1: encryptedShares[0].shareData, // For database 232 | encryptedShare2: encryptedShares[1].shareData, // For Redis 1 233 | encryptedShare3: encryptedShares[2]?.shareData, // For Redis 2 234 | shareIndices: [1, 2, 3], 235 | }; 236 | } 237 | 238 | export async function reconstructPrivateKey( 239 | userId: string, 240 | share1: string 241 | ): Promise { 242 | try { 243 | const shareAvailable = await ensureShareAvailability(userId, share1); 244 | if (!shareAvailable) { 245 | throw new Error( 246 | "Could not ensure share availability - wallet may be compromised" 247 | ); 248 | } 249 | 250 | const decryptedShare1 = decryptData(share1, MPC_CONFIG.ENCRYPTION_KEY); 251 | const shareBuffer1 = Buffer.from(decryptedShare1, "hex"); 252 | 253 | const redisKey2 = `mpc:share:${userId}:2`; 254 | const encryptedShare2 = await redis1.get(redisKey2); 255 | 256 | if (!encryptedShare2) { 257 | throw new Error( 258 | "Share 2 not found in Redis 1 - automatic recovery failed" 259 | ); 260 | } 261 | 262 | const decryptedShare2 = decryptData( 263 | encryptedShare2, 264 | MPC_CONFIG.ENCRYPTION_KEY 265 | ); 266 | const shareBuffer2 = Buffer.from(decryptedShare2, "hex"); 267 | 268 | const redisKey3 = `mpc:share:${userId}:3`; 269 | const encryptedShare3 = await redis2.get(redisKey3); 270 | 271 | if (!encryptedShare3) { 272 | throw new Error( 273 | "Share 3 not found in Redis 2 - automatic recovery failed" 274 | ); 275 | } 276 | 277 | const decryptedShare3 = decryptData( 278 | encryptedShare3, 279 | MPC_CONFIG.ENCRYPTION_KEY 280 | ); 281 | const shareBuffer3 = Buffer.from(decryptedShare3, "hex"); 282 | 283 | const reconstructedKey = sss.combine([ 284 | shareBuffer1, 285 | shareBuffer2, 286 | shareBuffer3, 287 | ]); 288 | 289 | const keypair = Keypair.fromSecretKey(reconstructedKey); 290 | 291 | return keypair; 292 | } catch (error) { 293 | console.error("Failed to reconstruct private key:", error); 294 | throw new Error( 295 | "Key reconstruction failed - need all 3 shares for threshold=3" 296 | ); 297 | } 298 | } 299 | 300 | export async function validateShares( 301 | userId: string, 302 | expectedPublicKey: string, 303 | share1: string 304 | ): Promise { 305 | try { 306 | const reconstructedKeypair = await reconstructPrivateKey(userId, share1); 307 | return reconstructedKeypair.publicKey.toBase58() === expectedPublicKey; 308 | } catch (error) { 309 | console.error("Share validation failed:", error); 310 | return false; 311 | } 312 | } 313 | 314 | export async function signTransactionMPC( 315 | userId: string, 316 | transaction: Transaction, 317 | encryptedShare1: string 318 | ): Promise { 319 | let keypair: Keypair | null = null; 320 | 321 | try { 322 | keypair = await reconstructPrivateKey(userId, encryptedShare1); 323 | transaction.feePayer = new PublicKey(keypair.publicKey.toBase58()); 324 | transaction.sign(keypair); 325 | const signature = transaction.signatures[0].signature; 326 | 327 | return transaction; 328 | } finally { 329 | if (keypair) { 330 | keypair.secretKey.fill(0); 331 | keypair = null; 332 | } 333 | } 334 | } 335 | 336 | export async function rotateKeyShares( 337 | userId: string, 338 | oldShare1: string 339 | ): Promise { 340 | const keypair = await reconstructPrivateKey(userId, oldShare1); 341 | 342 | const privateKeyBytes = keypair.secretKey; 343 | const publicKey = keypair.publicKey.toBase58(); 344 | 345 | const shares = sss.split(Buffer.from(privateKeyBytes), { 346 | shares: MPC_CONFIG.TOTAL_SHARES, 347 | threshold: MPC_CONFIG.THRESHOLD, 348 | }); 349 | 350 | const encryptedShares = shares.map((share, index) => ({ 351 | shareIndex: index + 1, 352 | shareData: encryptData(share.toString("hex"), MPC_CONFIG.ENCRYPTION_KEY), 353 | publicKey, 354 | })); 355 | 356 | const ttlSeconds = MPC_CONFIG.SHARE_TTL_DAYS * 24 * 60 * 60; 357 | const redisKey2 = `mpc:share:${userId}:2`; 358 | await redis1.setex(redisKey2, ttlSeconds, encryptedShares[1].shareData); 359 | 360 | const redisKey3 = `mpc:share:${userId}:3`; 361 | await redis2.setex(redisKey3, ttlSeconds, encryptedShares[2].shareData); 362 | 363 | keypair.secretKey.fill(0); 364 | 365 | return { 366 | publicKey, 367 | encryptedShare1: encryptedShares[0].shareData, 368 | encryptedShare2: encryptedShares[1].shareData, 369 | encryptedShare3: encryptedShares[2]?.shareData, 370 | shareIndices: [1, 2, 3], 371 | }; 372 | } 373 | 374 | export async function healthCheck(): Promise<{ 375 | redisConnected: boolean; 376 | redis1Connected: boolean; 377 | redis2Connected: boolean; 378 | encryptionWorking: boolean; 379 | shareReconstructionWorking: boolean; 380 | }> { 381 | try { 382 | const redis1Connected = (await redis1.ping()) === "PONG"; 383 | const redis2Connected = (await redis2.ping()) === "PONG"; 384 | const redisConnected = redis1Connected && redis2Connected; 385 | 386 | const testData = "test-encryption"; 387 | const encrypted = encryptData(testData, MPC_CONFIG.ENCRYPTION_KEY); 388 | const decrypted = decryptData(encrypted, MPC_CONFIG.ENCRYPTION_KEY); 389 | const encryptionWorking = decrypted === testData; 390 | 391 | const testKeypair = Keypair.generate(); 392 | const shares = sss.split(Buffer.from(testKeypair.secretKey), { 393 | shares: 3, 394 | threshold: 3, 395 | }); 396 | const reconstructed = sss.combine([shares[0], shares[1], shares[2]]); 397 | const shareReconstructionWorking = 398 | Buffer.compare(testKeypair.secretKey, reconstructed) === 0; 399 | 400 | return { 401 | redisConnected, 402 | redis1Connected, 403 | redis2Connected, 404 | encryptionWorking, 405 | shareReconstructionWorking, 406 | }; 407 | } catch (error) { 408 | console.error("MPC health check failed:", error); 409 | return { 410 | redisConnected: false, 411 | redis1Connected: false, 412 | redis2Connected: false, 413 | encryptionWorking: false, 414 | shareReconstructionWorking: false, 415 | }; 416 | } 417 | } --------------------------------------------------------------------------------