@cedros/login-sidecar 0.0.1

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/.env.example ADDED
@@ -0,0 +1,52 @@
1
+ # Privacy Cash Sidecar Configuration
2
+ #
3
+ # This sidecar handles Privacy Cash operations for SSS embedded wallets.
4
+ # User keypairs are reconstructed from SSS shares and passed to endpoints.
5
+
6
+ # Port to listen on (default: 3100)
7
+ PORT=3100
8
+
9
+ # Host/interface to bind to (default: 127.0.0.1)
10
+ # Use 0.0.0.0 if you need to accept connections from outside the machine/container.
11
+ HOST=127.0.0.1
12
+
13
+ # API key for authentication (required)
14
+ # Generate with: openssl rand -hex 32
15
+ SIDECAR_API_KEY=your-secure-api-key-here
16
+
17
+ # Solana network: 'devnet' or 'mainnet-beta' (default: devnet). 'mainnet' is accepted as an alias.
18
+ SOLANA_NETWORK=devnet
19
+
20
+ # Solana RPC URL (optional, defaults based on network)
21
+ # SOLANA_RPC_URL=https://api.devnet.solana.com
22
+
23
+ # Privacy Cash program ID (required)
24
+ # Mainnet: 9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
25
+ PRIVACY_CASH_PROGRAM_ID=9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
26
+
27
+ # Company wallet address for withdrawals (required)
28
+ # This is where funds are sent when withdrawing from user Privacy Cash accounts
29
+ COMPANY_WALLET_ADDRESS=
30
+
31
+ # =============================================================================
32
+ # Jupiter Ultra API (for gasless SPL token swaps)
33
+ # =============================================================================
34
+ # Jupiter API URL (default: https://api.jup.ag/ultra/v1)
35
+ # JUPITER_API_URL=https://api.jup.ag/ultra/v1
36
+
37
+ # Jupiter API key (REQUIRED - get free key at https://portal.jup.ag)
38
+ JUPITER_API_KEY=
39
+
40
+ # Minimum swap amount in USD for gasless swaps (default: 10)
41
+ # Jupiter requires ~$10 minimum for gasless swaps
42
+ JUPITER_MIN_SWAP_USD=10
43
+
44
+ # Jupiter Ultra API rate limit (requests per 10 seconds)
45
+ # Used for swap queue throttling. Ultra API uses dynamic rate limiting
46
+ # that scales with your 24-hour swap volume:
47
+ # $0 volume: 50 requests / 10 seconds
48
+ # $10,000 volume: 51 requests / 10 seconds
49
+ # $100,000 volume: 61 requests / 10 seconds
50
+ # $1M volume: 165 requests / 10 seconds
51
+ # Note: This is NOT tied to Pro plan tiers - it scales automatically
52
+ JUPITER_RATE_LIMIT=50
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @cedros/login-sidecar
2
+
3
+ Node.js sidecar service for `cedros-login-server`.
4
+
5
+ This service wraps the JavaScript-only Privacy Cash SDK and provides related Solana utilities
6
+ (verification, batching, swaps) used by the Rust backend.
7
+
8
+ ## Requirements
9
+
10
+ - Node.js >= 24
11
+
12
+ By default the sidecar binds to `127.0.0.1`. Set `HOST=0.0.0.0` if you need it reachable outside the machine/container.
13
+
14
+ ## Local dev
15
+
16
+ ```bash
17
+ cd login-sidecar
18
+ nvm use
19
+ npm install
20
+ npm run dev
21
+ ```
22
+
23
+ Configure the Rust server to point at the sidecar:
24
+
25
+ - `PRIVACY_CASH_SIDECAR_URL` (e.g. `http://localhost:3100`)
26
+ - `SIDECAR_API_KEY`
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Sidecar configuration loaded from environment variables
3
+ */
4
+ export interface Config {
5
+ port: number;
6
+ host: string;
7
+ apiKey: string;
8
+ solanaRpcUrl: string;
9
+ solanaNetwork: 'devnet' | 'mainnet-beta';
10
+ privacyCashProgramId: string;
11
+ /** Company wallet address for withdrawals (receives funds from Privacy Cash) */
12
+ companyWalletAddress: string;
13
+ /** Jupiter Ultra API settings for gasless swaps */
14
+ jupiter: {
15
+ apiUrl: string;
16
+ apiKey: string | null;
17
+ /** Minimum swap amount in USD (Jupiter gasless requires ~$10) */
18
+ minSwapUsd: number;
19
+ /** Rate limit in requests per 10 seconds (for swap queue throttling) */
20
+ rateLimit: number;
21
+ };
22
+ }
23
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Sidecar configuration loaded from environment variables
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadConfig = loadConfig;
7
+ function getEnvRequired(key) {
8
+ const value = process.env[key];
9
+ if (!value) {
10
+ throw new Error(`Missing required environment variable: ${key}`);
11
+ }
12
+ return value;
13
+ }
14
+ function getEnvOptional(key, defaultValue) {
15
+ return process.env[key] || defaultValue;
16
+ }
17
+ function loadConfig() {
18
+ const rawNetwork = getEnvOptional('SOLANA_NETWORK', 'devnet');
19
+ const network = rawNetwork === 'mainnet' ? 'mainnet-beta' : rawNetwork;
20
+ if (network !== 'devnet' && network !== 'mainnet-beta') {
21
+ throw new Error(`Invalid SOLANA_NETWORK: ${rawNetwork}. Must be 'devnet' or 'mainnet-beta' (or 'mainnet' alias)`);
22
+ }
23
+ return {
24
+ port: parseInt(getEnvOptional('PORT', '3100'), 10),
25
+ host: getEnvOptional('HOST', '127.0.0.1'),
26
+ apiKey: getEnvRequired('SIDECAR_API_KEY'),
27
+ solanaRpcUrl: getEnvOptional('SOLANA_RPC_URL', network === 'devnet'
28
+ ? 'https://api.devnet.solana.com'
29
+ : 'https://api.mainnet-beta.solana.com'),
30
+ solanaNetwork: network,
31
+ privacyCashProgramId: getEnvRequired('PRIVACY_CASH_PROGRAM_ID'),
32
+ companyWalletAddress: getEnvRequired('COMPANY_WALLET_ADDRESS'),
33
+ jupiter: {
34
+ apiUrl: getEnvOptional('JUPITER_API_URL', 'https://api.jup.ag/ultra/v1'),
35
+ apiKey: process.env.JUPITER_API_KEY || null,
36
+ minSwapUsd: parseFloat(getEnvOptional('JUPITER_MIN_SWAP_USD', '10')),
37
+ rateLimit: parseInt(getEnvOptional('JUPITER_RATE_LIMIT', '50'), 10),
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Cedros Login Sidecar
3
+ *
4
+ * A Node.js sidecar service used by the Rust backend. This wraps the Privacy Cash SDK
5
+ * and provides related Solana utilities (verification, batching, swaps).
6
+ * Required because the Privacy Cash SDK is JavaScript-only and requires Node.js 24+.
7
+ *
8
+ * Endpoints:
9
+ * - GET /health - Health check (no auth required)
10
+ * - POST /deposit/build - Build unsigned deposit transaction
11
+ * - POST /deposit/submit - Submit signed deposit transaction
12
+ * - POST /withdraw - Withdraw note to company wallet
13
+ *
14
+ * All endpoints except /health require Bearer token auth.
15
+ */
16
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Cedros Login Sidecar
4
+ *
5
+ * A Node.js sidecar service used by the Rust backend. This wraps the Privacy Cash SDK
6
+ * and provides related Solana utilities (verification, batching, swaps).
7
+ * Required because the Privacy Cash SDK is JavaScript-only and requires Node.js 24+.
8
+ *
9
+ * Endpoints:
10
+ * - GET /health - Health check (no auth required)
11
+ * - POST /deposit/build - Build unsigned deposit transaction
12
+ * - POST /deposit/submit - Submit signed deposit transaction
13
+ * - POST /withdraw - Withdraw note to company wallet
14
+ *
15
+ * All endpoints except /health require Bearer token auth.
16
+ */
17
+ var __importDefault = (this && this.__importDefault) || function (mod) {
18
+ return (mod && mod.__esModule) ? mod : { "default": mod };
19
+ };
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ const express_1 = __importDefault(require("express"));
22
+ const config_js_1 = require("./config.js");
23
+ const auth_js_1 = require("./middleware/auth.js");
24
+ const rateLimit_js_1 = require("./middleware/rateLimit.js");
25
+ const solana_js_1 = require("./services/solana.js");
26
+ const privacy_cash_js_1 = require("./services/privacy-cash.js");
27
+ const jupiter_js_1 = require("./services/jupiter.js");
28
+ const health_js_1 = require("./routes/health.js");
29
+ const deposit_js_1 = require("./routes/deposit.js");
30
+ const withdraw_js_1 = require("./routes/withdraw.js");
31
+ const batch_js_1 = require("./routes/batch.js");
32
+ const verify_js_1 = require("./routes/verify.js");
33
+ const redactRpcUrl_js_1 = require("./utils/redactRpcUrl.js");
34
+ async function main() {
35
+ console.log('[Sidecar] Starting Cedros login sidecar...');
36
+ // Load configuration
37
+ const config = (0, config_js_1.loadConfig)();
38
+ console.log(`[Sidecar] Network: ${config.solanaNetwork}`);
39
+ console.log(`[Sidecar] RPC: ${(0, redactRpcUrl_js_1.redactRpcUrl)(config.solanaRpcUrl)}`);
40
+ console.log(`[Sidecar] Host: ${config.host}`);
41
+ console.log(`[Sidecar] Port: ${config.port}`);
42
+ // Initialize services
43
+ const solanaService = new solana_js_1.SolanaService(config);
44
+ const privacyCashService = new privacy_cash_js_1.PrivacyCashService(config, solanaService);
45
+ const jupiterService = new jupiter_js_1.JupiterService(config);
46
+ // Verify RPC connection
47
+ const rpcConnected = await solanaService.isConnected();
48
+ if (!rpcConnected) {
49
+ console.error('[Sidecar] WARNING: Failed to connect to Solana RPC');
50
+ }
51
+ else {
52
+ console.log('[Sidecar] Connected to Solana RPC');
53
+ }
54
+ // Create Express app
55
+ const app = (0, express_1.default)();
56
+ // Middleware
57
+ app.use(express_1.default.json({ limit: '1mb' }));
58
+ // SIDE-02: Basic in-memory rate limiting (protects sidecar from accidental overload)
59
+ app.use((0, rateLimit_js_1.createRateLimitMiddleware)({ windowMs: 10_000, maxRequests: 200 }));
60
+ // SIDE-02: Request timeout guard (best-effort)
61
+ app.use((_req, res, next) => {
62
+ res.setTimeout(30_000, () => {
63
+ if (!res.headersSent) {
64
+ res.status(503).json({ error: 'Request timeout' });
65
+ }
66
+ });
67
+ next();
68
+ });
69
+ app.use((0, auth_js_1.createAuthMiddleware)(config.apiKey));
70
+ // Request logging (simple, non-blocking)
71
+ app.use((req, _res, next) => {
72
+ console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
73
+ next();
74
+ });
75
+ // Routes
76
+ app.use((0, health_js_1.createHealthRoutes)(solanaService, privacyCashService));
77
+ app.use((0, deposit_js_1.createDepositRoutes)(privacyCashService, jupiterService));
78
+ app.use((0, withdraw_js_1.createWithdrawRoutes)(privacyCashService, jupiterService));
79
+ app.use((0, verify_js_1.createVerifyRoutes)(solanaService));
80
+ app.use('/batch', (0, batch_js_1.createBatchRouter)(config));
81
+ // 404 handler
82
+ app.use((_req, res) => {
83
+ res.status(404).json({ error: 'Not found' });
84
+ });
85
+ // Error handler
86
+ app.use((err, _req, res, _next) => {
87
+ console.error('[Sidecar] Unhandled error:', err);
88
+ res.status(500).json({ error: 'Internal server error' });
89
+ });
90
+ // Start server
91
+ const server = app.listen(config.port, config.host, () => {
92
+ console.log(`[Sidecar] Server running on http://${config.host}:${config.port}`);
93
+ console.log('[Sidecar] Ready to accept requests');
94
+ });
95
+ // SIDE-02: Tighten server-level timeouts (slowloris + hung sockets)
96
+ server.keepAliveTimeout = 5_000;
97
+ server.headersTimeout = 15_000;
98
+ server.requestTimeout = 30_000;
99
+ }
100
+ main().catch((error) => {
101
+ console.error('[Sidecar] Fatal error:', error);
102
+ process.exit(1);
103
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * API key authentication middleware
3
+ *
4
+ * All endpoints (except /health) require a valid API key in the Authorization header.
5
+ * Format: Authorization: Bearer <api-key>
6
+ */
7
+ import { Request, Response, NextFunction } from 'express';
8
+ /**
9
+ * Create auth middleware with the given API key
10
+ */
11
+ export declare function createAuthMiddleware(apiKey: string): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * API key authentication middleware
4
+ *
5
+ * All endpoints (except /health) require a valid API key in the Authorization header.
6
+ * Format: Authorization: Bearer <api-key>
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createAuthMiddleware = createAuthMiddleware;
10
+ const crypto_1 = require("crypto");
11
+ /**
12
+ * Create auth middleware with the given API key
13
+ */
14
+ function createAuthMiddleware(apiKey) {
15
+ const apiKeyBuffer = Buffer.from(apiKey);
16
+ return (req, res, next) => {
17
+ // Skip auth for health check
18
+ if (req.path === '/health') {
19
+ return next();
20
+ }
21
+ const authHeader = req.headers.authorization;
22
+ if (!authHeader) {
23
+ res.status(401).json({ error: 'Missing Authorization header' });
24
+ return;
25
+ }
26
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
27
+ if (!match) {
28
+ res.status(401).json({ error: 'Invalid Authorization header format. Expected: Bearer <token>' });
29
+ return;
30
+ }
31
+ const providedKey = match[1];
32
+ const providedBuffer = Buffer.from(providedKey);
33
+ // Timing-safe comparison to prevent timing attacks
34
+ if (providedBuffer.length !== apiKeyBuffer.length ||
35
+ !(0, crypto_1.timingSafeEqual)(providedBuffer, apiKeyBuffer)) {
36
+ res.status(401).json({ error: 'Invalid API key' });
37
+ return;
38
+ }
39
+ next();
40
+ };
41
+ }
@@ -0,0 +1,6 @@
1
+ import type { NextFunction, Request, Response } from 'express';
2
+ export interface RateLimitOptions {
3
+ windowMs: number;
4
+ maxRequests: number;
5
+ }
6
+ export declare function createRateLimitMiddleware({ windowMs, maxRequests }: RateLimitOptions): (req: Request, res: Response, next: NextFunction) => void;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRateLimitMiddleware = createRateLimitMiddleware;
4
+ function createRateLimitMiddleware({ windowMs, maxRequests }) {
5
+ const buckets = new Map();
6
+ return (req, res, next) => {
7
+ if (req.path === '/health') {
8
+ next();
9
+ return;
10
+ }
11
+ const now = Date.now();
12
+ const ip = req.ip || req.socket.remoteAddress || 'unknown';
13
+ const existing = buckets.get(ip);
14
+ if (!existing || existing.resetAtMs <= now) {
15
+ buckets.set(ip, { count: 1, resetAtMs: now + windowMs });
16
+ next();
17
+ return;
18
+ }
19
+ existing.count += 1;
20
+ buckets.set(ip, existing);
21
+ if (existing.count > maxRequests) {
22
+ const retryAfterSeconds = Math.max(1, Math.ceil((existing.resetAtMs - now) / 1000));
23
+ res.setHeader('Retry-After', String(retryAfterSeconds));
24
+ res.status(429).json({ error: 'Too many requests' });
25
+ return;
26
+ }
27
+ next();
28
+ };
29
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Batch swap route for micro deposit batching
3
+ *
4
+ * POST /batch/swap - Swap SOL to output currency via Jupiter
5
+ *
6
+ * Used by the micro batch worker to convert accumulated SOL deposits
7
+ * into the company's preferred currency (USDC/USDT).
8
+ */
9
+ import { Router } from 'express';
10
+ import { Config } from '../config.js';
11
+ export declare function createBatchRouter(config: Config): Router;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * Batch swap route for micro deposit batching
4
+ *
5
+ * POST /batch/swap - Swap SOL to output currency via Jupiter
6
+ *
7
+ * Used by the micro batch worker to convert accumulated SOL deposits
8
+ * into the company's preferred currency (USDC/USDT).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.createBatchRouter = createBatchRouter;
12
+ const express_1 = require("express");
13
+ const jupiter_js_1 = require("../services/jupiter.js");
14
+ function createBatchRouter(config) {
15
+ const router = (0, express_1.Router)();
16
+ const jupiterService = new jupiter_js_1.JupiterService(config);
17
+ /**
18
+ * POST /batch/swap
19
+ *
20
+ * Swaps SOL from treasury wallet to output currency via Jupiter.
21
+ * Used for batching micro deposits.
22
+ */
23
+ router.post('/swap', async (req, res) => {
24
+ try {
25
+ const body = req.body;
26
+ // Validate request
27
+ if (!body.privateKey || typeof body.privateKey !== 'string') {
28
+ return res.status(400).json({
29
+ success: false,
30
+ error: 'Missing or invalid privateKey',
31
+ });
32
+ }
33
+ if (!body.amountLamports || typeof body.amountLamports !== 'number' || body.amountLamports <= 0) {
34
+ return res.status(400).json({
35
+ success: false,
36
+ error: 'Missing or invalid amountLamports',
37
+ });
38
+ }
39
+ if (!body.outputCurrency || !['USDC', 'USDT'].includes(body.outputCurrency.toUpperCase())) {
40
+ return res.status(400).json({
41
+ success: false,
42
+ error: 'outputCurrency must be USDC or USDT',
43
+ });
44
+ }
45
+ // Get output mint
46
+ const outputMint = jupiter_js_1.JupiterService.getMintForCurrency(body.outputCurrency);
47
+ if (!outputMint) {
48
+ return res.status(400).json({
49
+ success: false,
50
+ error: `Unsupported output currency: ${body.outputCurrency}`,
51
+ });
52
+ }
53
+ // Parse the keypair
54
+ let keypair;
55
+ try {
56
+ keypair = jupiter_js_1.JupiterService.parseKeypair(body.privateKey);
57
+ }
58
+ catch (e) {
59
+ console.error('[Batch] Failed to parse private key:', e);
60
+ return res.status(400).json({
61
+ success: false,
62
+ error: 'Invalid private key format',
63
+ });
64
+ }
65
+ console.log(`[Batch] Executing swap: ${body.amountLamports} lamports SOL -> ${body.outputCurrency}`);
66
+ // Execute the swap
67
+ const result = await jupiterService.swapFromSol(outputMint, body.amountLamports.toString(), keypair);
68
+ if (!result.success) {
69
+ console.error(`[Batch] Swap failed: ${result.error}`);
70
+ return res.status(500).json({
71
+ success: false,
72
+ txSignature: result.txSignature || '',
73
+ inputLamports: body.amountLamports,
74
+ outputAmount: '0',
75
+ outputCurrency: body.outputCurrency.toUpperCase(),
76
+ error: result.error || 'Swap execution failed',
77
+ });
78
+ }
79
+ console.log(`[Batch] Swap succeeded: signature=${result.txSignature}, output=${result.actualOutAmount || result.expectedOutAmount}`);
80
+ const response = {
81
+ success: true,
82
+ txSignature: result.txSignature,
83
+ inputLamports: body.amountLamports,
84
+ outputAmount: result.actualOutAmount || result.expectedOutAmount,
85
+ outputCurrency: body.outputCurrency.toUpperCase(),
86
+ };
87
+ return res.json(response);
88
+ }
89
+ catch (error) {
90
+ console.error('[Batch] Unexpected error:', error);
91
+ return res.status(500).json({
92
+ success: false,
93
+ error: error instanceof Error ? error.message : 'Internal server error',
94
+ });
95
+ }
96
+ });
97
+ return router;
98
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Deposit endpoints for Privacy Cash deposits (SSS embedded wallets only)
3
+ *
4
+ * POST /deposit - Execute a deposit to the user's Privacy Cash account
5
+ * POST /deposit/swap-and-deposit - Swap SPL token to SOL and deposit (gasless)
6
+ *
7
+ * Architecture:
8
+ * - User's keypair is reconstructed server-side from SSS shares
9
+ * - Deposit goes to user's Privacy Cash account (user's pubkey)
10
+ * - Server stores Share B during privacy period for later withdrawal
11
+ */
12
+ import { Router } from 'express';
13
+ import { PrivacyCashService } from '../services/privacy-cash.js';
14
+ import { JupiterService } from '../services/jupiter.js';
15
+ export declare function createDepositRoutes(privacyCash: PrivacyCashService, jupiter: JupiterService): Router;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * Deposit endpoints for Privacy Cash deposits (SSS embedded wallets only)
4
+ *
5
+ * POST /deposit - Execute a deposit to the user's Privacy Cash account
6
+ * POST /deposit/swap-and-deposit - Swap SPL token to SOL and deposit (gasless)
7
+ *
8
+ * Architecture:
9
+ * - User's keypair is reconstructed server-side from SSS shares
10
+ * - Deposit goes to user's Privacy Cash account (user's pubkey)
11
+ * - Server stores Share B during privacy period for later withdrawal
12
+ */
13
+ var __importDefault = (this && this.__importDefault) || function (mod) {
14
+ return (mod && mod.__esModule) ? mod : { "default": mod };
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.createDepositRoutes = createDepositRoutes;
18
+ const express_1 = require("express");
19
+ const web3_js_1 = require("@solana/web3.js");
20
+ const bs58_1 = __importDefault(require("bs58"));
21
+ function createDepositRoutes(privacyCash, jupiter) {
22
+ const router = (0, express_1.Router)();
23
+ /**
24
+ * POST /deposit
25
+ *
26
+ * Execute a Privacy Cash deposit for an SSS embedded wallet.
27
+ *
28
+ * The deposit goes to the USER's Privacy Cash account (user's pubkey is owner).
29
+ * This provides privacy because the subsequent withdrawal to company wallet
30
+ * is unlinkable on-chain.
31
+ *
32
+ * Requirements:
33
+ * - User must have no-recovery wallet (no Share C)
34
+ * - Server temporarily stores Share B during privacy period
35
+ */
36
+ router.post('/deposit', async (req, res) => {
37
+ try {
38
+ const body = req.body;
39
+ // Validate request
40
+ if (!body.user_private_key || typeof body.user_private_key !== 'string') {
41
+ res.status(400).json({ error: 'user_private_key is required and must be a base58 string' });
42
+ return;
43
+ }
44
+ if (!body.amount_lamports || typeof body.amount_lamports !== 'number') {
45
+ res.status(400).json({ error: 'amount_lamports is required and must be a number' });
46
+ return;
47
+ }
48
+ if (body.amount_lamports <= 0) {
49
+ res.status(400).json({ error: 'amount_lamports must be positive' });
50
+ return;
51
+ }
52
+ // Decode the private key
53
+ let userKeypair;
54
+ try {
55
+ const privateKeyBytes = bs58_1.default.decode(body.user_private_key);
56
+ userKeypair = web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
57
+ }
58
+ catch {
59
+ res.status(400).json({ error: 'Invalid private key format' });
60
+ return;
61
+ }
62
+ // Execute the deposit to user's Privacy Cash account
63
+ const result = await privacyCash.executeDeposit(userKeypair, body.amount_lamports);
64
+ res.json({
65
+ success: result.success,
66
+ tx_signature: result.txSignature,
67
+ user_pubkey: userKeypair.publicKey.toBase58(),
68
+ });
69
+ }
70
+ catch (error) {
71
+ console.error('[Deposit] Error:', error);
72
+ res.status(500).json({
73
+ error: 'Failed to execute deposit',
74
+ details: error instanceof Error ? error.message : 'Unknown error',
75
+ });
76
+ }
77
+ });
78
+ /**
79
+ * POST /deposit/swap-and-deposit
80
+ *
81
+ * Swap SPL token to SOL using Jupiter gasless swap, then deposit to Privacy Cash.
82
+ *
83
+ * This endpoint combines two operations:
84
+ * 1. Gasless swap: SPL token → SOL (Jupiter pays gas fees)
85
+ * 2. Privacy Cash deposit: SOL → user's Privacy Cash account
86
+ *
87
+ * Requirements:
88
+ * - User wallet must have < 0.01 SOL (gasless requirement)
89
+ * - Trade size must be > ~$10 USD (Jupiter minimum)
90
+ * - User must have no-recovery wallet (no Share C)
91
+ */
92
+ router.post('/deposit/swap-and-deposit', async (req, res) => {
93
+ try {
94
+ const body = req.body;
95
+ // Validate request
96
+ if (!body.user_private_key || typeof body.user_private_key !== 'string') {
97
+ res.status(400).json({ error: 'user_private_key is required and must be a base58 string' });
98
+ return;
99
+ }
100
+ if (!body.input_mint || typeof body.input_mint !== 'string') {
101
+ res.status(400).json({ error: 'input_mint is required and must be a string' });
102
+ return;
103
+ }
104
+ if (!body.amount || typeof body.amount !== 'string') {
105
+ res.status(400).json({ error: 'amount is required and must be a string' });
106
+ return;
107
+ }
108
+ // Decode the private key
109
+ let userKeypair;
110
+ try {
111
+ const privateKeyBytes = bs58_1.default.decode(body.user_private_key);
112
+ userKeypair = web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
113
+ }
114
+ catch {
115
+ res.status(400).json({ error: 'Invalid private key format' });
116
+ return;
117
+ }
118
+ const userPubkey = userKeypair.publicKey.toBase58();
119
+ console.log(`[SwapAndDeposit] Starting for user: ${userPubkey}`);
120
+ // Step 1: Execute gasless swap (SPL token → SOL)
121
+ console.log(`[SwapAndDeposit] Swapping ${body.amount} of ${body.input_mint} to SOL`);
122
+ const swapResult = await jupiter.swapToSol(body.input_mint, body.amount, userKeypair);
123
+ if (!swapResult.success) {
124
+ console.error(`[SwapAndDeposit] Swap failed: ${swapResult.error} (code: ${swapResult.errorCode})`);
125
+ res.status(500).json({
126
+ error: 'Swap failed',
127
+ details: swapResult.error,
128
+ error_code: swapResult.errorCode,
129
+ swap_tx_signature: swapResult.txSignature || null,
130
+ gasless: swapResult.gasless,
131
+ });
132
+ return;
133
+ }
134
+ // Use actual output if available, otherwise expected
135
+ const solAmount = swapResult.actualOutAmount || swapResult.expectedOutAmount;
136
+ console.log(`[SwapAndDeposit] Swap succeeded: ${swapResult.txSignature}, got ${solAmount} lamports (gasless: ${swapResult.gasless})`);
137
+ // Step 2: Execute Privacy Cash deposit with the swapped SOL
138
+ const solAmountLamports = parseInt(solAmount, 10);
139
+ console.log(`[SwapAndDeposit] Depositing ${solAmountLamports} lamports to Privacy Cash`);
140
+ const depositResult = await privacyCash.executeDeposit(userKeypair, solAmountLamports);
141
+ console.log(`[SwapAndDeposit] Deposit succeeded: ${depositResult.txSignature}`);
142
+ res.json({
143
+ success: true,
144
+ swap_tx_signature: swapResult.txSignature,
145
+ deposit_tx_signature: depositResult.txSignature,
146
+ sol_amount_lamports: solAmountLamports,
147
+ gasless: swapResult.gasless,
148
+ // Return input (pre-swap) amount for crediting
149
+ input_mint: body.input_mint,
150
+ input_amount: body.amount,
151
+ user_pubkey: userPubkey,
152
+ });
153
+ }
154
+ catch (error) {
155
+ console.error('[SwapAndDeposit] Error:', error);
156
+ res.status(500).json({
157
+ error: 'Failed to execute swap and deposit',
158
+ details: error instanceof Error ? error.message : 'Unknown error',
159
+ });
160
+ }
161
+ });
162
+ return router;
163
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Health check endpoint
3
+ *
4
+ * GET /health - Returns service health status
5
+ */
6
+ import { Router } from 'express';
7
+ import { SolanaService } from '../services/solana.js';
8
+ import { PrivacyCashService } from '../services/privacy-cash.js';
9
+ export declare function createHealthRoutes(solana: SolanaService, privacyCash: PrivacyCashService): Router;