@qhristen/paygrid 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +126 -0
  2. package/dist/api/index.d.ts +10 -0
  3. package/dist/api/index.js +98 -0
  4. package/dist/auth/actions.d.ts +4 -0
  5. package/dist/auth/actions.js +37 -0
  6. package/dist/auth/index.d.ts +12 -0
  7. package/dist/auth/index.js +28 -0
  8. package/dist/auth/login-screen.d.ts +2 -0
  9. package/dist/auth/login-screen.jsx +49 -0
  10. package/dist/blockchain/index.d.ts +11 -0
  11. package/dist/blockchain/index.js +59 -0
  12. package/dist/checkout/index.d.ts +10 -0
  13. package/dist/checkout/index.jsx +235 -0
  14. package/dist/client.d.ts +3 -0
  15. package/dist/client.js +4 -0
  16. package/dist/config/index.d.ts +39 -0
  17. package/dist/config/index.js +44 -0
  18. package/dist/core/paygrid.d.ts +44 -0
  19. package/dist/core/paygrid.js +238 -0
  20. package/dist/core/privacy-wrapper.d.ts +29 -0
  21. package/dist/core/privacy-wrapper.js +72 -0
  22. package/dist/dashboard/api-section.d.ts +7 -0
  23. package/dist/dashboard/api-section.jsx +146 -0
  24. package/dist/dashboard/constant.d.ts +13 -0
  25. package/dist/dashboard/constant.jsx +13 -0
  26. package/dist/dashboard/index.d.ts +3 -0
  27. package/dist/dashboard/index.jsx +319 -0
  28. package/dist/dashboard/payment-table.d.ts +8 -0
  29. package/dist/dashboard/payment-table.jsx +85 -0
  30. package/dist/db/index.d.ts +15 -0
  31. package/dist/db/index.js +174 -0
  32. package/dist/index.css +2 -0
  33. package/dist/index.d.ts +3 -0
  34. package/dist/index.js +6 -0
  35. package/dist/server.d.ts +3 -0
  36. package/dist/server.js +4 -0
  37. package/dist/tsconfig.tsbuildinfo +1 -0
  38. package/dist/types/index.d.ts +65 -0
  39. package/dist/types/index.js +20 -0
  40. package/package.json +60 -0
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # PayGrid
2
+
3
+ **PayGrid** is a self-hosted, embeddable Web3 payments infrastructure for Solana. It allows you to accept SOL and SPL token payments directly within your Next.js application without relying on third-party payment processors.
4
+
5
+ ---
6
+
7
+ ## 🚀 Features
8
+
9
+ - **Self-Hosted:** Full control over your data and treasury keys.
10
+ - **Dual Payment Flows:**
11
+ - **Wallet-Signing:** Direct transaction signing with Solana wallets (Phantom, Solflare, etc.).
12
+ - **Manual Transfer:** Unique temporary wallet generation for manual transfers.
13
+ - **Embedded Dashboard:** Built-in React components for managing payments and API keys.
14
+ - **SQLite Powered:** No heavy database setup required; data is stored locally in an embedded SQLite file.
15
+ - **Status Monitoring:** Background watcher automatically settles payments by monitoring the blockchain.
16
+
17
+ ---
18
+
19
+ ## 🛠 Installation
20
+
21
+ ```bash
22
+ npm install @paygrid/core
23
+ ```
24
+
25
+ ---
26
+
27
+ ## 🏗 Setup
28
+
29
+ ### 1. Environment Variables
30
+
31
+ Create a `.env` file in your Next.js project:
32
+
33
+ ```bash
34
+ SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
35
+ TREASURY_PRIVATE_KEY=YourBase58PrivateKey...
36
+ NEXT_PUBLIC_PAYGRID_API_SECRET=at_least_32_character_random_string_for_paygrid_test
37
+ =at_least_32_character_random_string
38
+ DB_PATH=./paygrid.db # Default
39
+ NETWORK=mainnet-beta # Or devnet
40
+ ```
41
+
42
+ ### 2. Initialize PayGrid (API Route)
43
+
44
+ Create a file at `app/api/paygrid/[...path]/route.ts`:
45
+
46
+ ```typescript
47
+ import { initPayGrid } from '@paygrid/core';
48
+ import { createApiHandler } from '@paygrid/core/api';
49
+
50
+ // Initialize core
51
+ const paygrid = await initPayGrid();
52
+
53
+ // Create handler
54
+ const handler = createApiHandler(paygrid);
55
+
56
+ export { handler as GET, handler as POST };
57
+ ```
58
+
59
+ ### 3. Embed the Dashboard
60
+
61
+ Create a dashboard page at `app/dashboard/payments/page.tsx`:
62
+
63
+ ```tsx
64
+ 'use client';
65
+
66
+ import { PayGridDashboard } from '@paygrid/core';
67
+ import '@paygrid/core/dist/index.css'; // Import the isolated styles
68
+
69
+ export default function DashboardPage() {
70
+ return (
71
+ <div className="pg-dashboard-wrapper">
72
+ <PayGridDashboard apiUrl="/api/paygrid" />
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 💳 Usage
81
+
82
+ ### Create a Payment Intent
83
+
84
+ ```typescript
85
+ const response = await fetch('/api/paygrid/payment-intents', {
86
+ method: 'POST',
87
+ headers: {
88
+ 'x-api-key': 'your_api_key',
89
+ 'Content-Type': 'application/json'
90
+ },
91
+ body: JSON.stringify({
92
+ amount: 1.5,
93
+ tokenMint: 'SOL',
94
+ method: 'wallet-signing', // or 'manual-transfer'
95
+ metadata: { orderId: '12345' }
96
+ })
97
+ });
98
+
99
+ const intent = await response.json();
100
+ console.log('Payment Intent Created:', intent.id);
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 🔐 Security
106
+
107
+ - **Treasury Keys:** Your treasury private key is only used to sign transactions for settlements (if applicable) and is never exposed via the API.
108
+ - **API Keys:** Keys are hashed using `bcrypt` before being stored in the database.
109
+ - **Validation:** Every transaction is validated against the blockchain state before being marked as `settled`.
110
+
111
+ ---
112
+
113
+ ## 🧱 Architecture
114
+
115
+ - `core/`: Payment lifecycle and state machine logic.
116
+ - `blockchain/`: Solana Web3.js integration and watchers.
117
+ - `db/`: Embedded SQLite storage.
118
+ - `api/`: Next.js request handlers.
119
+ - `dashboard/`: React UI components.
120
+ - `auth/`: API key generation and validation.
121
+
122
+ ---
123
+
124
+ ## ⚖️ License
125
+
126
+ MIT
@@ -0,0 +1,10 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { PayGrid } from "../core/paygrid";
3
+ export declare function createApiHandler(paygrid: PayGrid): (req: NextRequest) => Promise<NextResponse<import("../types").PaymentIntent> | NextResponse<import("../types").PaymentIntent[]> | NextResponse<import("../types").AnalyticsData> | NextResponse<{
4
+ key: string;
5
+ apiKey: import("../types").ApiKey;
6
+ }> | NextResponse<import("../types").ApiKey[]> | NextResponse<{
7
+ success: boolean;
8
+ }> | NextResponse<import("@radr/shadowwire").WithdrawResponse | undefined> | NextResponse<void> | NextResponse<{
9
+ error: any;
10
+ }>>;
@@ -0,0 +1,98 @@
1
+ import { NextResponse } from "next/server";
2
+ export function createApiHandler(paygrid) {
3
+ return async function handler(req) {
4
+ const { pathname } = new URL(req.url);
5
+ const pathParts = pathname.split("/").filter(Boolean);
6
+ const resource = pathParts[2]; // /api/paygrid/[resource]
7
+ const id = pathParts[3];
8
+ // Auth check
9
+ const apiKey = req.headers.get("x-api-key");
10
+ const isValid = apiKey ? await paygrid.validateApiKey(apiKey) : false;
11
+ if (!isValid) {
12
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
13
+ }
14
+ try {
15
+ if (resource === "payment-intents") {
16
+ if (req.method === "POST") {
17
+ const body = await req.json();
18
+ const intent = await paygrid.createPaymentIntent(body);
19
+ return NextResponse.json(intent);
20
+ }
21
+ if (req.method === "GET" && id) {
22
+ const intent = await paygrid.getPayment(id);
23
+ if (!intent)
24
+ return NextResponse.json({ error: "Not Found" }, { status: 404 });
25
+ return NextResponse.json(intent);
26
+ }
27
+ }
28
+ if (resource === "payments") {
29
+ if (req.method === "GET") {
30
+ const payments = await paygrid.getPayments();
31
+ return NextResponse.json(payments);
32
+ }
33
+ }
34
+ if (resource === "analytics") {
35
+ if (req.method === "GET") {
36
+ const { searchParams } = new URL(req.url);
37
+ const days = parseInt(searchParams.get("days") || "30", 10);
38
+ const stats = await paygrid.getAnalytics(days);
39
+ return NextResponse.json(stats);
40
+ }
41
+ }
42
+ if (resource === "api-keys") {
43
+ if (req.method === "POST") {
44
+ const { name } = await req.json();
45
+ const result = await paygrid.createApiKey(name);
46
+ return NextResponse.json(result);
47
+ }
48
+ if (req.method === "GET") {
49
+ const keys = await paygrid.listApiKeys();
50
+ return NextResponse.json(keys);
51
+ }
52
+ if (req.method === "DELETE" && id) {
53
+ await paygrid.deleteApiKey(id);
54
+ return NextResponse.json({ success: true });
55
+ }
56
+ }
57
+ if (resource === "privacy-withdraw") {
58
+ if (req.method === "POST") {
59
+ const body = await req.json();
60
+ const { tokenSymbol, recipient } = body;
61
+ if (!tokenSymbol || !recipient) {
62
+ return NextResponse.json({ error: "Missing parameters" }, { status: 400 });
63
+ }
64
+ const result = await paygrid.withdrawFromPrivacy({
65
+ tokenSymbol: tokenSymbol,
66
+ recipient,
67
+ });
68
+ return NextResponse.json(result);
69
+ }
70
+ }
71
+ if (resource === "privacy-transfer") {
72
+ if (req.method === "POST") {
73
+ const body = await req.json();
74
+ const { tokenSymbol, amount, sender } = body;
75
+ if (!tokenSymbol) {
76
+ return NextResponse.json({ error: "Missing parameters" }, { status: 400 });
77
+ }
78
+ const result = await paygrid.transferFromPrivacy({
79
+ tokenSymbol: tokenSymbol,
80
+ amount,
81
+ sender,
82
+ });
83
+ return NextResponse.json(result);
84
+ }
85
+ }
86
+ return NextResponse.json({ error: "Method Not Allowed" }, { status: 405 });
87
+ }
88
+ catch (error) {
89
+ console.error("API Error Details:", {
90
+ message: error.message,
91
+ stack: error.stack,
92
+ resource,
93
+ method: req.method,
94
+ });
95
+ return NextResponse.json({ error: error.message || "Internal Server Error" }, { status: 500 });
96
+ }
97
+ };
98
+ }
@@ -0,0 +1,4 @@
1
+ export declare function login(prevState: any, formData: FormData): Promise<{
2
+ error: string;
3
+ }>;
4
+ export declare function logout(): Promise<void>;
@@ -0,0 +1,37 @@
1
+ 'use server';
2
+ import { cookies } from 'next/headers';
3
+ import { redirect } from 'next/navigation';
4
+ export async function login(prevState, formData) {
5
+ const email = formData.get('email');
6
+ const password = formData.get('password');
7
+ const adminEmail = process.env.NEXT_PUBLIC_ADMIN_EMAIL;
8
+ const adminPassword = process.env.NEXT_PUBLIC_ADMIN_PASSWORD;
9
+ if (!adminEmail || !adminPassword) {
10
+ return { error: 'Server configuration error: Admin credentials not set in environment.' };
11
+ }
12
+ if (email === adminEmail && password === adminPassword) {
13
+ const cookieStore = await cookies();
14
+ cookieStore.set('paygrid_admin_session', 'true', {
15
+ httpOnly: false,
16
+ secure: process.env.NODE_ENV === 'production',
17
+ path: '/',
18
+ maxAge: 60 * 60 * 24 // 1 day
19
+ });
20
+ redirect('/dashboard');
21
+ }
22
+ else {
23
+ return { error: 'Invalid email or password' };
24
+ }
25
+ }
26
+ export async function logout() {
27
+ const cookieStore = await cookies();
28
+ // Delete the admin session cookie with same path and immediate expiration
29
+ cookieStore.set('paygrid_admin_session', '', {
30
+ httpOnly: false,
31
+ secure: process.env.NODE_ENV === 'production',
32
+ path: '/',
33
+ maxAge: 0,
34
+ });
35
+ cookieStore.delete("paygrid_admin_session");
36
+ redirect('/');
37
+ }
@@ -0,0 +1,12 @@
1
+ import { ApiKey } from '../types';
2
+ export declare class AuthService {
3
+ private apiSecret;
4
+ constructor(apiSecret: string);
5
+ generateApiKey(name: string): {
6
+ key: string;
7
+ apiKey: ApiKey;
8
+ };
9
+ verifyApiKey(rawKey: string, hashedKey: string): boolean;
10
+ }
11
+ export * from './login-screen';
12
+ export * from './actions';
@@ -0,0 +1,28 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import crypto from 'crypto';
3
+ export class AuthService {
4
+ constructor(apiSecret) {
5
+ this.apiSecret = apiSecret;
6
+ }
7
+ generateApiKey(name) {
8
+ const rawKey = `pg_${crypto.randomBytes(32).toString('hex')}`;
9
+ const id = crypto.randomUUID();
10
+ const keyHint = rawKey.substring(0, 7); // pg_ + 4 chars
11
+ const hashedKey = bcrypt.hashSync(rawKey, 10);
12
+ return {
13
+ key: rawKey,
14
+ apiKey: {
15
+ id,
16
+ keyHint,
17
+ hashedKey,
18
+ name,
19
+ createdAt: Date.now(),
20
+ }
21
+ };
22
+ }
23
+ verifyApiKey(rawKey, hashedKey) {
24
+ return bcrypt.compareSync(rawKey, hashedKey);
25
+ }
26
+ }
27
+ export * from './login-screen';
28
+ export * from './actions';
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export declare function LoginScreen(): React.JSX.Element;
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import React, { useActionState } from 'react';
3
+ import { login } from './actions';
4
+ import { Loader2 } from 'lucide-react';
5
+ export function LoginScreen() {
6
+ const [state, formAction, isPending] = useActionState(login, null);
7
+ return (<div className="min-h-screen flex items-center justify-center bg-[#050505] text-white overflow-hidden relative">
8
+ <div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-indigo-900/20 via-[#050505] to-[#050505] z-0 pointer-events-none"/>
9
+
10
+ <div className="w-full max-w-md p-8 rounded-2xl border border-white/10 bg-[#111]/80 backdrop-blur-xl shadow-2xl z-10">
11
+ <div className="mb-8 text-center">
12
+ <div className="mx-auto w-12 h-12 bg-indigo-500 rounded-xl flex items-center justify-center mb-4 shadow-lg shadow-indigo-500/20">
13
+ <span className="text-xl font-bold text-white">P</span>
14
+ </div>
15
+ <h1 className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-400">Welcome Back</h1>
16
+ <p className="text-gray-500 text-sm mt-2">Sign in to your PayGrid dashboard</p>
17
+ </div>
18
+
19
+ <form action={formAction} className="space-y-4">
20
+ <div className="space-y-2">
21
+ <label htmlFor="email" className="text-sm font-medium text-gray-300">Email</label>
22
+ <input id="email" name="email" type="email" required placeholder="admin@paygrid.com" className="w-full bg-black/50 border border-white/10 rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition-all placeholder:text-gray-600 hover:border-white/20"/>
23
+ </div>
24
+
25
+ <div className="space-y-2">
26
+ <label htmlFor="password" className="text-sm font-medium text-gray-300">Password</label>
27
+ <input id="password" name="password" type="password" required placeholder="••••••••" className="w-full bg-black/50 border border-white/10 rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition-all placeholder:text-gray-600 hover:border-white/20"/>
28
+ </div>
29
+
30
+ {state?.error && (<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm text-center">
31
+ {state.error}
32
+ </div>)}
33
+
34
+ <button type="submit" disabled={isPending} className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-medium py-3 rounded-lg transition-all shadow-lg shadow-indigo-600/20 flex items-center justify-center gap-2 mt-6 disabled:opacity-50 disabled:cursor-not-allowed">
35
+ {isPending ? (<>
36
+ <Loader2 className="w-4 h-4 animate-spin"/>
37
+ Signing in...
38
+ </>) : ("Sign In")}
39
+ </button>
40
+
41
+ <div className="mt-6 text-center">
42
+ <p className="text-xs text-gray-600">
43
+ Secure admin access only
44
+ </p>
45
+ </div>
46
+ </form>
47
+ </div>
48
+ </div>);
49
+ }
@@ -0,0 +1,11 @@
1
+ export declare class SolanaService {
2
+ private connection;
3
+ constructor(rpcUrl: string);
4
+ generateTemporaryWallet(): Promise<{
5
+ publicKey: string;
6
+ privateKey: string;
7
+ }>;
8
+ checkBalance(publicKey: string): Promise<number>;
9
+ confirmTransaction(signature: string): Promise<boolean>;
10
+ findTransferTo(targetWallet: string, amount: number): Promise<string | null>;
11
+ }
@@ -0,0 +1,59 @@
1
+ import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
2
+ import bs58 from "bs58";
3
+ export class SolanaService {
4
+ constructor(rpcUrl) {
5
+ this.connection = new Connection(rpcUrl, "confirmed");
6
+ }
7
+ async generateTemporaryWallet() {
8
+ const keypair = Keypair.generate();
9
+ return {
10
+ publicKey: keypair.publicKey.toBase58(),
11
+ privateKey: bs58.encode(keypair.secretKey),
12
+ };
13
+ }
14
+ async checkBalance(publicKey) {
15
+ const balance = await this.connection.getBalance(new PublicKey(publicKey));
16
+ return balance / LAMPORTS_PER_SOL;
17
+ }
18
+ async confirmTransaction(signature) {
19
+ try {
20
+ const result = await this.connection.confirmTransaction(signature, "confirmed");
21
+ return !result.value.err;
22
+ }
23
+ catch (error) {
24
+ console.error("Error confirming transaction:", error);
25
+ return false;
26
+ }
27
+ }
28
+ async findTransferTo(targetWallet, amount) {
29
+ const pubkey = new PublicKey(targetWallet);
30
+ const signatures = await this.connection.getSignaturesForAddress(pubkey, {
31
+ limit: 10,
32
+ });
33
+ for (const sigInfo of signatures) {
34
+ const tx = await this.connection.getTransaction(sigInfo.signature, {
35
+ commitment: "confirmed",
36
+ maxSupportedTransactionVersion: 0, // 👈 REQUIRED now
37
+ });
38
+ if (!tx)
39
+ continue;
40
+ // Basic SOL transfer check - this is simplified for now
41
+ // A more robust check would involve parsing instructions
42
+ const meta = tx.meta;
43
+ if (!meta)
44
+ continue;
45
+ const preBalances = meta.preBalances;
46
+ const postBalances = meta.postBalances;
47
+ const accountKeys = tx.transaction.message.staticAccountKeys;
48
+ const targetIndex = accountKeys.findIndex((key) => key.equals(pubkey));
49
+ if (targetIndex === -1)
50
+ continue;
51
+ const receivedAmount = (postBalances[targetIndex] - preBalances[targetIndex]) /
52
+ LAMPORTS_PER_SOL;
53
+ if (receivedAmount >= amount) {
54
+ return sigInfo.signature;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+ }
@@ -0,0 +1,10 @@
1
+ import { PayGridResponseType } from "../types";
2
+ export interface CheckoutModalProps {
3
+ amount: number;
4
+ method: "wallet-signing" | "manual-transfer";
5
+ tokenSymbol: string;
6
+ sender: string;
7
+ onPaymentResponse?: (response: PayGridResponseType) => void;
8
+ }
9
+ export declare function CheckoutModal({ amount: propsAmount, method: propsMethod, tokenSymbol: propsTokenSymbol, sender: propsSender, onPaymentResponse, }: CheckoutModalProps): import("react").JSX.Element;
10
+ export default CheckoutModal;