@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 +52 -0
- package/README.md +26 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +40 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +103 -0
- package/dist/middleware/auth.d.ts +11 -0
- package/dist/middleware/auth.js +41 -0
- package/dist/middleware/rateLimit.d.ts +6 -0
- package/dist/middleware/rateLimit.js +29 -0
- package/dist/routes/batch.d.ts +11 -0
- package/dist/routes/batch.js +98 -0
- package/dist/routes/deposit.d.ts +15 -0
- package/dist/routes/deposit.js +163 -0
- package/dist/routes/health.d.ts +9 -0
- package/dist/routes/health.js +37 -0
- package/dist/routes/verify.d.ts +2 -0
- package/dist/routes/verify.js +48 -0
- package/dist/routes/withdraw.d.ts +15 -0
- package/dist/routes/withdraw.js +137 -0
- package/dist/services/jupiter.d.ts +129 -0
- package/dist/services/jupiter.js +304 -0
- package/dist/services/privacy-cash.d.ts +84 -0
- package/dist/services/privacy-cash.js +185 -0
- package/dist/services/solana.d.ts +39 -0
- package/dist/services/solana.js +71 -0
- package/dist/utils/fetchWithTimeout.d.ts +2 -0
- package/dist/utils/fetchWithTimeout.js +17 -0
- package/dist/utils/redactRpcUrl.d.ts +1 -0
- package/dist/utils/redactRpcUrl.js +12 -0
- package/dist/utils/verifySolTransfer.d.ts +22 -0
- package/dist/utils/verifySolTransfer.js +80 -0
- package/package.json +27 -0
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`
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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;
|