@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.
- package/README.md +126 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +98 -0
- package/dist/auth/actions.d.ts +4 -0
- package/dist/auth/actions.js +37 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +28 -0
- package/dist/auth/login-screen.d.ts +2 -0
- package/dist/auth/login-screen.jsx +49 -0
- package/dist/blockchain/index.d.ts +11 -0
- package/dist/blockchain/index.js +59 -0
- package/dist/checkout/index.d.ts +10 -0
- package/dist/checkout/index.jsx +235 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +4 -0
- package/dist/config/index.d.ts +39 -0
- package/dist/config/index.js +44 -0
- package/dist/core/paygrid.d.ts +44 -0
- package/dist/core/paygrid.js +238 -0
- package/dist/core/privacy-wrapper.d.ts +29 -0
- package/dist/core/privacy-wrapper.js +72 -0
- package/dist/dashboard/api-section.d.ts +7 -0
- package/dist/dashboard/api-section.jsx +146 -0
- package/dist/dashboard/constant.d.ts +13 -0
- package/dist/dashboard/constant.jsx +13 -0
- package/dist/dashboard/index.d.ts +3 -0
- package/dist/dashboard/index.jsx +319 -0
- package/dist/dashboard/payment-table.d.ts +8 -0
- package/dist/dashboard/payment-table.jsx +85 -0
- package/dist/db/index.d.ts +15 -0
- package/dist/db/index.js +174 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +4 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/index.d.ts +65 -0
- package/dist/types/index.js +20 -0
- package/package.json +60 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { SUPPORTED_TOKENS } from "../config";
|
|
4
|
+
import { PaymentMethod } from "../types";
|
|
5
|
+
import { initWASM, isWASMSupported, ShadowWireClient, } from "@radr/shadowwire";
|
|
6
|
+
export function CheckoutModal({ amount: propsAmount, method: propsMethod, tokenSymbol: propsTokenSymbol, sender: propsSender, onPaymentResponse, }) {
|
|
7
|
+
const [isCheckoutOpen, setIsCheckoutOpen] = useState(false);
|
|
8
|
+
const [step, setStep] = useState("config");
|
|
9
|
+
const [selectedToken, setSelectedToken] = useState(propsTokenSymbol);
|
|
10
|
+
const [selectedMethod, setSelectedMethod] = useState(propsMethod === "manual-transfer"
|
|
11
|
+
? PaymentMethod.MANUAL_TRANSFER
|
|
12
|
+
: PaymentMethod.WALLET_SIGNING);
|
|
13
|
+
const [client] = useState(() => new ShadowWireClient());
|
|
14
|
+
const [amount, setAmount] = useState(propsAmount);
|
|
15
|
+
const [activeIntent, setActiveIntent] = useState(null);
|
|
16
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const apiKey = process.env.NEXT_PUBLIC_PAYGRID_API_SECRET;
|
|
19
|
+
const [wasmInitialized, setWasmInitialized] = useState(false);
|
|
20
|
+
const [balance, setBalance] = useState(null);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
async function init() {
|
|
23
|
+
if (!isWASMSupported()) {
|
|
24
|
+
setError("WebAssembly not supported");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await initWASM("/wasm/settler_wasm_bg.wasm");
|
|
29
|
+
setWasmInitialized(true);
|
|
30
|
+
await loadBalance();
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
setError("Initialization failed: " + err.message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
init();
|
|
37
|
+
}, []);
|
|
38
|
+
const loadBalance = async () => {
|
|
39
|
+
try {
|
|
40
|
+
const data = await client.getBalance(propsSender, selectedToken);
|
|
41
|
+
setBalance(data.available / 1e9);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error("Balance load failed:", err);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const handleStartPayment = async () => {
|
|
48
|
+
try {
|
|
49
|
+
setIsLoading(true);
|
|
50
|
+
setError(null);
|
|
51
|
+
const headers = {};
|
|
52
|
+
if (apiKey)
|
|
53
|
+
headers["x-api-key"] = apiKey;
|
|
54
|
+
const response = await fetch("/api/paygrid/payment-intents", {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: {
|
|
57
|
+
...headers,
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
amount,
|
|
62
|
+
method: propsMethod,
|
|
63
|
+
tokenSymbol: selectedToken,
|
|
64
|
+
sender: propsSender,
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
// setStep("paying");
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
throw new Error(`Payment intent creation failed: ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
const data = (await response.json());
|
|
72
|
+
if (onPaymentResponse) {
|
|
73
|
+
onPaymentResponse(data);
|
|
74
|
+
}
|
|
75
|
+
setActiveIntent(data);
|
|
76
|
+
setStep("paying");
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const errorMessage = err instanceof Error ? err.message : "An error occurred";
|
|
80
|
+
setError(errorMessage);
|
|
81
|
+
console.error("Payment error:", err);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
setIsLoading(false);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return (<div>
|
|
88
|
+
<button onClick={() => setIsCheckoutOpen(true)} className="bg-white/5 hover:bg-white/10 cursor-pointer border border-white/10 text-white px-8 py-4 rounded-2xl font-bold text-lg transition-all">
|
|
89
|
+
Pay now
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
{isCheckoutOpen && (<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
93
|
+
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={() => setIsCheckoutOpen(false)}></div>
|
|
94
|
+
|
|
95
|
+
<div className="relative w-full max-w-md bg-[#111] border border-white/10 rounded-3xl overflow-hidden shadow-2xl">
|
|
96
|
+
<div className="p-6">
|
|
97
|
+
<div className="flex justify-between items-center mb-6">
|
|
98
|
+
<div className="flex items-center gap-2">
|
|
99
|
+
<div className="w-6 h-6 bg-indigo-600 rounded flex items-center justify-center text-[10px] font-bold">
|
|
100
|
+
P
|
|
101
|
+
</div>
|
|
102
|
+
<span className="font-bold">PayGrid checkout</span>
|
|
103
|
+
</div>
|
|
104
|
+
<button onClick={() => setIsCheckoutOpen(false)} className="text-gray-500 cursor-pointer hover:text-white">
|
|
105
|
+
×
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{step === "config" && (<div className="space-y-6">
|
|
110
|
+
<div>
|
|
111
|
+
<label className="text-xs text-gray-500 font-bold uppercase block mb-2">
|
|
112
|
+
Select Token
|
|
113
|
+
</label>
|
|
114
|
+
<div className="grid grid-cols-3 gap-2">
|
|
115
|
+
{SUPPORTED_TOKENS.map((token) => (<button key={token.symbol} disabled={token.disabled} onClick={() => setSelectedToken(token.symbol)} className={`p-3 rounded-xl cursor-pointer border transition-all flex flex-col items-center gap-1 ${selectedToken === token.symbol ? "border-indigo-500 bg-indigo-500/10" : "border-white/5 bg-white/5 hover:border-white/20"}`}>
|
|
116
|
+
<div className="w-6 h-6 rounded-full" style={{ backgroundColor: token.color }}></div>
|
|
117
|
+
<span className="text-sm font-medium">
|
|
118
|
+
{token.symbol}
|
|
119
|
+
</span>
|
|
120
|
+
</button>))}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div>
|
|
125
|
+
<label className="text-xs text-gray-500 font-bold uppercase block mb-2">
|
|
126
|
+
Payment Method
|
|
127
|
+
</label>
|
|
128
|
+
<div className="space-y-2">
|
|
129
|
+
<button onClick={() => setSelectedMethod(PaymentMethod.WALLET_SIGNING)} className={`w-full p-4 cursor-pointer rounded-xl border text-left flex items-center gap-3 transition-all ${selectedMethod === PaymentMethod.WALLET_SIGNING ? "border-indigo-500 bg-indigo-500/10" : "border-white/5 bg-white/5"}`}>
|
|
130
|
+
<div className="p-2 bg-white/5 rounded-lg">⚡</div>
|
|
131
|
+
<div>
|
|
132
|
+
<p className="text-sm font-semibold">
|
|
133
|
+
Wallet Signing
|
|
134
|
+
</p>
|
|
135
|
+
<p className="text-xs text-gray-500">
|
|
136
|
+
Sign with Phantom, Solflare or Backpack
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
</button>
|
|
140
|
+
<button onClick={() => setSelectedMethod(PaymentMethod.MANUAL_TRANSFER)} disabled className={`w-full p-4 cursor-pointer rounded-xl border text-left flex items-center gap-3 transition-all ${selectedMethod === PaymentMethod.MANUAL_TRANSFER ? "border-indigo-500 bg-indigo-500/10" : "border-white/5 bg-white/5"}`}>
|
|
141
|
+
<div className="p-2 bg-white/5 rounded-lg">📋</div>
|
|
142
|
+
<div>
|
|
143
|
+
<p className="text-sm font-semibold">
|
|
144
|
+
Manual Transfer
|
|
145
|
+
</p>
|
|
146
|
+
<p className="text-xs text-gray-500">
|
|
147
|
+
Send tokens to a unique address
|
|
148
|
+
</p>
|
|
149
|
+
</div>
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="pt-4 border-t border-white/10">
|
|
155
|
+
<div className="flex justify-between items-end mb-4">
|
|
156
|
+
<span className="text-gray-400 text-sm">Total Due</span>
|
|
157
|
+
<div className="text-right">
|
|
158
|
+
<span className="text-2xl font-bold">
|
|
159
|
+
{amount} {selectedToken}
|
|
160
|
+
</span>
|
|
161
|
+
<p className="text-xs text-gray-500">{`~${amount} ${selectedToken}`}</p>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
{error && (<div className="bg-red-500/10 border border-red-500/20 p-3 rounded-lg mb-4">
|
|
165
|
+
<p className="text-xs text-red-400">{error}</p>
|
|
166
|
+
</div>)}
|
|
167
|
+
<button onClick={handleStartPayment} disabled={isLoading} className="w-full bg-white text-black py-4 rounded-2xl font-bold hover:bg-gray-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
|
168
|
+
{isLoading ? "Processing..." : "Confirm & Pay"}
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
</div>)}
|
|
172
|
+
|
|
173
|
+
{step === "paying" && (<div className="text-center py-8">
|
|
174
|
+
<div className="relative w-24 h-24 mx-auto mb-6">
|
|
175
|
+
<div className="absolute inset-0 border-4 border-indigo-500/20 rounded-full"></div>
|
|
176
|
+
<div className="absolute inset-0 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
|
|
177
|
+
<div className="absolute inset-0 flex items-center justify-center font-bold text-lg">
|
|
178
|
+
{selectedToken}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<h3 className="text-xl font-bold mb-2">
|
|
182
|
+
Awaiting Transaction
|
|
183
|
+
</h3>
|
|
184
|
+
<p className="text-gray-500 text-sm mb-6 max-w-[280px] mx-auto">
|
|
185
|
+
Please complete the transaction in your wallet. We are
|
|
186
|
+
monitoring the Solana network.
|
|
187
|
+
</p>
|
|
188
|
+
<div className="bg-white/5 border border-white/10 p-4 rounded-2xl font-mono text-xs text-left">
|
|
189
|
+
<div className="flex justify-between mb-1">
|
|
190
|
+
<span className="text-gray-500">Network</span>
|
|
191
|
+
<span>Solana Mainnet</span>
|
|
192
|
+
</div>
|
|
193
|
+
<div className="flex justify-between mb-1">
|
|
194
|
+
<span className="text-gray-500">Status</span>
|
|
195
|
+
<span className="text-amber-400">Monitoring...</span>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="flex justify-between">
|
|
198
|
+
<span className="text-gray-500">Intent ID</span>
|
|
199
|
+
<span>{activeIntent?.id}</span>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>)}
|
|
203
|
+
|
|
204
|
+
{step === "success" && (<div className="text-center py-8">
|
|
205
|
+
<div className="w-20 h-20 bg-emerald-500/20 text-emerald-500 rounded-full mx-auto flex items-center justify-center text-4xl mb-6">
|
|
206
|
+
✓
|
|
207
|
+
</div>
|
|
208
|
+
<h3 className="text-2xl font-bold mb-2">Payment Settled!</h3>
|
|
209
|
+
<p className="text-gray-400 text-sm mb-6">
|
|
210
|
+
Your transaction has been confirmed on the blockchain.
|
|
211
|
+
</p>
|
|
212
|
+
|
|
213
|
+
{/* <div className="space-y-3 text-left">
|
|
214
|
+
<div className="bg-white/5 p-4 rounded-2xl border border-white/5">
|
|
215
|
+
<p className="text-[10px] text-gray-500 uppercase font-bold mb-1">
|
|
216
|
+
Transaction Hash
|
|
217
|
+
</p>
|
|
218
|
+
<p className="text-xs font-mono text-indigo-400 truncate">
|
|
219
|
+
{activeIntent?.transactionSignature}
|
|
220
|
+
</p>
|
|
221
|
+
</div>
|
|
222
|
+
</div> */}
|
|
223
|
+
</div>)}
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="bg-white/5 p-4 text-center">
|
|
227
|
+
<p className="text-[10px] text-gray-500 font-medium">
|
|
228
|
+
Powering the decentralized privacy economy with PayGrid
|
|
229
|
+
</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>)}
|
|
233
|
+
</div>);
|
|
234
|
+
}
|
|
235
|
+
export default CheckoutModal;
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { PayGridConfig } from '../types';
|
|
3
|
+
export declare const configSchema: z.ZodObject<{
|
|
4
|
+
SOLANA_RPC_URL: z.ZodString;
|
|
5
|
+
MERCHANT_WALLET_ADDRESS: z.ZodString;
|
|
6
|
+
DB_PATH: z.ZodDefault<z.ZodString>;
|
|
7
|
+
NEXT_PUBLIC_PAYGRID_API_SECRET: z.ZodString;
|
|
8
|
+
ADMIN_EMAIL: z.ZodString;
|
|
9
|
+
ADMIN_PASSWORD: z.ZodString;
|
|
10
|
+
NETWORK: z.ZodDefault<z.ZodEnum<["mainnet-beta", "devnet", "testnet"]>>;
|
|
11
|
+
}, "strip", z.ZodTypeAny, {
|
|
12
|
+
NEXT_PUBLIC_PAYGRID_API_SECRET: string;
|
|
13
|
+
SOLANA_RPC_URL: string;
|
|
14
|
+
MERCHANT_WALLET_ADDRESS: string;
|
|
15
|
+
DB_PATH: string;
|
|
16
|
+
ADMIN_EMAIL: string;
|
|
17
|
+
ADMIN_PASSWORD: string;
|
|
18
|
+
NETWORK: "mainnet-beta" | "devnet" | "testnet";
|
|
19
|
+
}, {
|
|
20
|
+
NEXT_PUBLIC_PAYGRID_API_SECRET: string;
|
|
21
|
+
SOLANA_RPC_URL: string;
|
|
22
|
+
MERCHANT_WALLET_ADDRESS: string;
|
|
23
|
+
ADMIN_EMAIL: string;
|
|
24
|
+
ADMIN_PASSWORD: string;
|
|
25
|
+
DB_PATH?: string | undefined;
|
|
26
|
+
NETWORK?: "mainnet-beta" | "devnet" | "testnet" | undefined;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function validateConfig(): PayGridConfig;
|
|
29
|
+
export declare const CONSTANTS: {
|
|
30
|
+
PAYMENT_EXPIRY_MS: number;
|
|
31
|
+
CHECK_INTERVAL_MS: number;
|
|
32
|
+
CONFIRMATIONS_REQUIRED: number;
|
|
33
|
+
};
|
|
34
|
+
export declare const SUPPORTED_TOKENS: {
|
|
35
|
+
symbol: string;
|
|
36
|
+
mint: string;
|
|
37
|
+
color: string;
|
|
38
|
+
disabled: boolean;
|
|
39
|
+
}[];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const configSchema = z.object({
|
|
3
|
+
SOLANA_RPC_URL: z.string().url(),
|
|
4
|
+
MERCHANT_WALLET_ADDRESS: z.string().min(44), // Base58 encoded private key
|
|
5
|
+
DB_PATH: z.string().default('./paygrid.db'),
|
|
6
|
+
NEXT_PUBLIC_PAYGRID_API_SECRET: z.string().min(32),
|
|
7
|
+
ADMIN_EMAIL: z.string().email(),
|
|
8
|
+
ADMIN_PASSWORD: z.string().min(5),
|
|
9
|
+
NETWORK: z.enum(['mainnet-beta', 'devnet', 'testnet']).default('mainnet-beta'),
|
|
10
|
+
});
|
|
11
|
+
export function validateConfig() {
|
|
12
|
+
const result = configSchema.safeParse({
|
|
13
|
+
SOLANA_RPC_URL: process.env.NEXT_PUBLIC_SOLANA_RPC_URL,
|
|
14
|
+
MERCHANT_WALLET_ADDRESS: process.env.NEXT_PUBLIC_MERCHANT_WALLET_ADDRESS,
|
|
15
|
+
DB_PATH: process.env.DB_PATH,
|
|
16
|
+
NEXT_PUBLIC_PAYGRID_API_SECRET: process.env.NEXT_PUBLIC_PAYGRID_API_SECRET,
|
|
17
|
+
NETWORK: process.env.NEXT_PUBLIC_NETWORK,
|
|
18
|
+
ADMIN_EMAIL: process.env.NEXT_PUBLIC_ADMIN_EMAIL,
|
|
19
|
+
ADMIN_PASSWORD: process.env.NEXT_PUBLIC_ADMIN_PASSWORD,
|
|
20
|
+
});
|
|
21
|
+
if (!result.success) {
|
|
22
|
+
console.error('❌ Invalid PayGrid Configuration:', result.error.format());
|
|
23
|
+
throw new Error(`Invalid PayGrid configuration: ${result.error.message}`);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
rpcUrl: result.data.SOLANA_RPC_URL,
|
|
27
|
+
marchantWalletADdress: result.data.MERCHANT_WALLET_ADDRESS,
|
|
28
|
+
dbPath: result.data.DB_PATH,
|
|
29
|
+
apiSecret: result.data.NEXT_PUBLIC_PAYGRID_API_SECRET,
|
|
30
|
+
network: result.data.NETWORK,
|
|
31
|
+
adminEmail: result.data.ADMIN_EMAIL,
|
|
32
|
+
adminPassword: result.data.ADMIN_PASSWORD
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export const CONSTANTS = {
|
|
36
|
+
PAYMENT_EXPIRY_MS: 30 * 60 * 1000, // 30 minutes
|
|
37
|
+
CHECK_INTERVAL_MS: 10 * 1000, // 10 seconds
|
|
38
|
+
CONFIRMATIONS_REQUIRED: 1,
|
|
39
|
+
};
|
|
40
|
+
export const SUPPORTED_TOKENS = [
|
|
41
|
+
{ symbol: 'SOL', mint: '11111111111111111111111111111111', color: '#14F195', disabled: true },
|
|
42
|
+
{ symbol: 'USDC', mint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', color: '#2775CA', disabled: false },
|
|
43
|
+
{ symbol: 'BONK', mint: 'DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263', color: '#FFA500', disabled: true }
|
|
44
|
+
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { AnalyticsData, PayGridConfig, PaymentIntent, TokenSymbol } from "../types";
|
|
2
|
+
import { DepositResponse } from "@radr/shadowwire";
|
|
3
|
+
export declare class PayGrid {
|
|
4
|
+
private db;
|
|
5
|
+
private solana;
|
|
6
|
+
private privacy;
|
|
7
|
+
private auth;
|
|
8
|
+
private config;
|
|
9
|
+
private watcherInterval?;
|
|
10
|
+
constructor(config: PayGridConfig);
|
|
11
|
+
init(): Promise<void>;
|
|
12
|
+
startWatcher(): Promise<void>;
|
|
13
|
+
stopWatcher(): void;
|
|
14
|
+
private checkPendingPayments;
|
|
15
|
+
createPaymentIntent(params: {
|
|
16
|
+
amount: number;
|
|
17
|
+
tokenSymbol: TokenSymbol;
|
|
18
|
+
method: "wallet-signing" | "manual-transfer";
|
|
19
|
+
sender?: string;
|
|
20
|
+
metadata?: Record<string, any>;
|
|
21
|
+
}): Promise<PaymentIntent & {
|
|
22
|
+
depositResponse: DepositResponse;
|
|
23
|
+
}>;
|
|
24
|
+
getPayment(id: string): Promise<PaymentIntent | undefined>;
|
|
25
|
+
createApiKey(name: string): Promise<{
|
|
26
|
+
key: string;
|
|
27
|
+
apiKey: import("../types").ApiKey;
|
|
28
|
+
}>;
|
|
29
|
+
validateApiKey(rawKey: string): Promise<boolean>;
|
|
30
|
+
listApiKeys(): Promise<import("../types").ApiKey[]>;
|
|
31
|
+
deleteApiKey(id: string): Promise<void>;
|
|
32
|
+
getPayments(): Promise<PaymentIntent[]>;
|
|
33
|
+
getAnalytics(days?: number): Promise<AnalyticsData>;
|
|
34
|
+
withdrawFromPrivacy(params: {
|
|
35
|
+
tokenSymbol: TokenSymbol;
|
|
36
|
+
recipient: string;
|
|
37
|
+
}): Promise<import("@radr/shadowwire").WithdrawResponse | undefined>;
|
|
38
|
+
transferFromPrivacy(params: {
|
|
39
|
+
tokenSymbol: TokenSymbol;
|
|
40
|
+
sender: string;
|
|
41
|
+
amount: number;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export declare function initPayGrid(config?: Partial<PayGridConfig>): Promise<PayGrid>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import { AuthService } from "../auth";
|
|
3
|
+
import { SolanaService } from "../blockchain";
|
|
4
|
+
import { CONSTANTS, SUPPORTED_TOKENS, validateConfig } from "../config";
|
|
5
|
+
import { PayGridDB } from "../db";
|
|
6
|
+
import { PaymentMethod, PaymentStatus, TokenSymbol, } from "../types";
|
|
7
|
+
import { PrivacyWrapper } from "./privacy-wrapper";
|
|
8
|
+
export class PayGrid {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.db = new PayGridDB(this.config.dbPath);
|
|
12
|
+
this.solana = new SolanaService(this.config.rpcUrl);
|
|
13
|
+
// Initialize Privacy Wrapper
|
|
14
|
+
this.privacy = new PrivacyWrapper(this.config);
|
|
15
|
+
this.auth = new AuthService(this.config.apiSecret);
|
|
16
|
+
}
|
|
17
|
+
async init() {
|
|
18
|
+
await this.db.init();
|
|
19
|
+
}
|
|
20
|
+
async startWatcher() {
|
|
21
|
+
console.log("🚀 PayGrid Watcher started");
|
|
22
|
+
// Run immediately once
|
|
23
|
+
await this.checkPendingPayments();
|
|
24
|
+
this.watcherInterval = setInterval(async () => {
|
|
25
|
+
try {
|
|
26
|
+
await this.checkPendingPayments();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error("PayGrid Watcher Error:", error);
|
|
30
|
+
}
|
|
31
|
+
}, CONSTANTS.CHECK_INTERVAL_MS);
|
|
32
|
+
}
|
|
33
|
+
stopWatcher() {
|
|
34
|
+
if (this.watcherInterval) {
|
|
35
|
+
clearInterval(this.watcherInterval);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async checkPendingPayments() {
|
|
39
|
+
const pending = await this.db.getPendingPayments();
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const payment of pending) {
|
|
42
|
+
if (payment.expiresAt < now) {
|
|
43
|
+
await this.db.updatePaymentStatus(payment.id, "expired");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (payment.method === PaymentMethod.MANUAL_TRANSFER &&
|
|
47
|
+
payment.walletAddress) {
|
|
48
|
+
const signature = await this.solana.findTransferTo(payment.walletAddress, payment.amount);
|
|
49
|
+
if (signature) {
|
|
50
|
+
await this.db.updatePaymentStatus(payment.id, "settled", signature);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (payment.status === "pending_confirmation" &&
|
|
54
|
+
payment.transactionSignature) {
|
|
55
|
+
const confirmed = await this.solana.confirmTransaction(payment.transactionSignature);
|
|
56
|
+
if (confirmed) {
|
|
57
|
+
await this.db.updatePaymentStatus(payment.id, "settled");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async createPaymentIntent(params) {
|
|
63
|
+
try {
|
|
64
|
+
const id = crypto.randomUUID();
|
|
65
|
+
// Resolve token mint if symbol is provided
|
|
66
|
+
let mintAddress;
|
|
67
|
+
let symbol = params.tokenSymbol;
|
|
68
|
+
if (params.tokenSymbol === TokenSymbol.SOL) {
|
|
69
|
+
mintAddress = SUPPORTED_TOKENS.find((t) => t.symbol === TokenSymbol.SOL)?.mint;
|
|
70
|
+
}
|
|
71
|
+
else if (params.tokenSymbol === TokenSymbol.USDC) {
|
|
72
|
+
mintAddress = SUPPORTED_TOKENS.find((t) => t.symbol === TokenSymbol.USDC)?.mint;
|
|
73
|
+
}
|
|
74
|
+
else if (params.tokenSymbol === TokenSymbol.BONK) {
|
|
75
|
+
mintAddress = SUPPORTED_TOKENS.find((t) => t.symbol === TokenSymbol.BONK)?.mint;
|
|
76
|
+
}
|
|
77
|
+
let response;
|
|
78
|
+
if (params.method === "wallet-signing") {
|
|
79
|
+
try {
|
|
80
|
+
const result = await this.privacy.createDepositTransaction({
|
|
81
|
+
amount: params.amount,
|
|
82
|
+
tokenMint: mintAddress ?? "",
|
|
83
|
+
symbol: params.tokenSymbol,
|
|
84
|
+
walletAddress: params.sender ?? "",
|
|
85
|
+
});
|
|
86
|
+
response = result;
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
console.error("Failed to create privacy transaction", e);
|
|
90
|
+
throw new Error("Failed to create privacy transaction: " + e.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const amount = symbol === TokenSymbol.SOL
|
|
94
|
+
? (response?.amount ?? 0) / 1e9
|
|
95
|
+
: (response?.amount ?? 0) / 1e6;
|
|
96
|
+
const payment = {
|
|
97
|
+
id,
|
|
98
|
+
amount: params.method === "wallet-signing" ? amount : params.amount,
|
|
99
|
+
tokenMint: mintAddress?.toString() ?? "",
|
|
100
|
+
tokenSymbol: symbol,
|
|
101
|
+
method: params.method,
|
|
102
|
+
status: PaymentStatus.AWAITING_PAYMENT,
|
|
103
|
+
walletAddress: this.config.marchantWalletADdress,
|
|
104
|
+
destination: this.config.marchantWalletADdress,
|
|
105
|
+
sender: params.sender,
|
|
106
|
+
expiresAt: Date.now() + CONSTANTS.PAYMENT_EXPIRY_MS,
|
|
107
|
+
createdAt: Date.now(),
|
|
108
|
+
metadata: params.metadata,
|
|
109
|
+
};
|
|
110
|
+
await this.db.createPayment(payment);
|
|
111
|
+
return { ...payment, depositResponse: response };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error("Error creating payment intent:", error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async getPayment(id) {
|
|
119
|
+
return await this.db.getPayment(id);
|
|
120
|
+
}
|
|
121
|
+
async createApiKey(name) {
|
|
122
|
+
const { key, apiKey } = this.auth.generateApiKey(name);
|
|
123
|
+
await this.db.createApiKey(apiKey);
|
|
124
|
+
return { key, apiKey };
|
|
125
|
+
}
|
|
126
|
+
async validateApiKey(rawKey) {
|
|
127
|
+
const keys = await this.db.listApiKeys();
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
if (this.auth.verifyApiKey(rawKey, key.hashedKey)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
async listApiKeys() {
|
|
136
|
+
return await this.db.listApiKeys();
|
|
137
|
+
}
|
|
138
|
+
async deleteApiKey(id) {
|
|
139
|
+
return await this.db.deleteApiKey(id);
|
|
140
|
+
}
|
|
141
|
+
async getPayments() {
|
|
142
|
+
return await this.db.getAllPayments();
|
|
143
|
+
}
|
|
144
|
+
async getAnalytics(days = 30) {
|
|
145
|
+
const payments = await this.db.getAllPayments();
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const startTime = now - days * 24 * 60 * 60 * 1000;
|
|
148
|
+
const settled = payments.filter((p) => p.status === "settled");
|
|
149
|
+
// Current period metrics
|
|
150
|
+
const filteredSettled = settled.filter((p) => p.createdAt >= startTime);
|
|
151
|
+
const totalRevenue = filteredSettled.reduce((sum, p) => sum + p.amount, 0);
|
|
152
|
+
// Previous period metrics
|
|
153
|
+
const prevStartTime = startTime - days * 24 * 60 * 60 * 1000;
|
|
154
|
+
const prevPeriodSettled = settled.filter((p) => p.createdAt >= prevStartTime && p.createdAt < startTime);
|
|
155
|
+
const pastRevenue = prevPeriodSettled.reduce((sum, p) => sum + p.amount, 0);
|
|
156
|
+
let revenueGrowth = 0;
|
|
157
|
+
if (pastRevenue > 0) {
|
|
158
|
+
revenueGrowth = ((totalRevenue - pastRevenue) / pastRevenue) * 100;
|
|
159
|
+
}
|
|
160
|
+
else if (totalRevenue > 0) {
|
|
161
|
+
revenueGrowth = 100;
|
|
162
|
+
}
|
|
163
|
+
// Group by date for history
|
|
164
|
+
const historyMap = new Map();
|
|
165
|
+
// Initialize map with all dates in the range to ensure zero values are shown
|
|
166
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
167
|
+
const d = new Date(now - i * 24 * 60 * 60 * 1000);
|
|
168
|
+
const dateStr = d.toLocaleDateString("en-US", {
|
|
169
|
+
month: "short",
|
|
170
|
+
day: "numeric",
|
|
171
|
+
});
|
|
172
|
+
historyMap.set(dateStr, 0);
|
|
173
|
+
}
|
|
174
|
+
filteredSettled.forEach((p) => {
|
|
175
|
+
const date = new Date(p.createdAt).toLocaleDateString("en-US", {
|
|
176
|
+
month: "short",
|
|
177
|
+
day: "numeric",
|
|
178
|
+
});
|
|
179
|
+
if (historyMap.has(date)) {
|
|
180
|
+
historyMap.set(date, (historyMap.get(date) || 0) + p.amount);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const history = Array.from(historyMap.entries()).map(([date, amount]) => ({
|
|
184
|
+
date,
|
|
185
|
+
amount,
|
|
186
|
+
}));
|
|
187
|
+
// Filter all payments for the period for transactionCount and settlementRate calculations
|
|
188
|
+
const filteredPayments = payments.filter((p) => p.createdAt >= startTime);
|
|
189
|
+
const filteredSettledCount = filteredPayments.filter((p) => p.status === "settled").length;
|
|
190
|
+
return {
|
|
191
|
+
totalRevenue,
|
|
192
|
+
revenueGrowth,
|
|
193
|
+
pastRevenue,
|
|
194
|
+
transactionCount: filteredPayments.length,
|
|
195
|
+
settlementRate: filteredPayments.length > 0
|
|
196
|
+
? (filteredSettledCount / filteredPayments.length) * 100
|
|
197
|
+
: 0,
|
|
198
|
+
history,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Withdraw funds from the privacy pool to a recipient
|
|
202
|
+
async withdrawFromPrivacy(params) {
|
|
203
|
+
try {
|
|
204
|
+
const res = await this.privacy.withdraw({
|
|
205
|
+
recipient: params.recipient,
|
|
206
|
+
symbol: params.tokenSymbol,
|
|
207
|
+
});
|
|
208
|
+
console.log(res, "withdrawal response");
|
|
209
|
+
return res;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
console.error("Error withdrawing from privacy:", error);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async transferFromPrivacy(params) {
|
|
217
|
+
try {
|
|
218
|
+
const res = await this.privacy.transfer({
|
|
219
|
+
symbol: params.tokenSymbol,
|
|
220
|
+
amount: params.amount,
|
|
221
|
+
sender: params.sender
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.error("Error withdrawing from privacy:", error);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export async function initPayGrid(config) {
|
|
231
|
+
const fullConfig = config
|
|
232
|
+
? { ...validateConfig(), ...config }
|
|
233
|
+
: validateConfig();
|
|
234
|
+
const paygrid = new PayGrid(fullConfig);
|
|
235
|
+
await paygrid.init();
|
|
236
|
+
await paygrid.startWatcher();
|
|
237
|
+
return paygrid;
|
|
238
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PayGridConfig } from "../types";
|
|
2
|
+
import { DepositResponse, TokenSymbol, TransferResponse } from "@radr/shadowwire";
|
|
3
|
+
export declare class PrivacyWrapper {
|
|
4
|
+
private client;
|
|
5
|
+
private rpcUrl;
|
|
6
|
+
private config;
|
|
7
|
+
constructor(config: PayGridConfig);
|
|
8
|
+
getPrivateBalance(walletAddress: string, symbol: TokenSymbol): Promise<import("@radr/shadowwire").PoolBalance>;
|
|
9
|
+
withdraw({ symbol, recipient }: {
|
|
10
|
+
symbol: string;
|
|
11
|
+
recipient: string;
|
|
12
|
+
}): Promise<import("@radr/shadowwire").WithdrawResponse | undefined>;
|
|
13
|
+
transfer({ symbol, sender, amount, }: {
|
|
14
|
+
symbol: string;
|
|
15
|
+
sender: string;
|
|
16
|
+
amount: number;
|
|
17
|
+
}): Promise<TransferResponse>;
|
|
18
|
+
createTransferTransaction(params: {
|
|
19
|
+
amount: number;
|
|
20
|
+
walletAddress: string;
|
|
21
|
+
symbol: TokenSymbol;
|
|
22
|
+
}): Promise<TransferResponse>;
|
|
23
|
+
createDepositTransaction(params: {
|
|
24
|
+
amount: number;
|
|
25
|
+
walletAddress: string;
|
|
26
|
+
tokenMint: string;
|
|
27
|
+
symbol: string;
|
|
28
|
+
}): Promise<DepositResponse>;
|
|
29
|
+
}
|