@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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Health check endpoint
|
|
4
|
+
*
|
|
5
|
+
* GET /health - Returns service health status
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createHealthRoutes = createHealthRoutes;
|
|
9
|
+
const express_1 = require("express");
|
|
10
|
+
function createHealthRoutes(solana, privacyCash) {
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
router.get('/health', async (_req, res) => {
|
|
13
|
+
try {
|
|
14
|
+
const rpcConnected = await solana.isConnected();
|
|
15
|
+
const sdkLoaded = await privacyCash.isLoaded();
|
|
16
|
+
const status = rpcConnected && sdkLoaded ? 'healthy' : 'degraded';
|
|
17
|
+
res.json({
|
|
18
|
+
status,
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
network: solana.getNetwork(),
|
|
21
|
+
checks: {
|
|
22
|
+
rpc_connected: rpcConnected,
|
|
23
|
+
sdk_loaded: sdkLoaded,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error('[Health] Check failed:', error);
|
|
29
|
+
res.status(503).json({
|
|
30
|
+
status: 'unhealthy',
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return router;
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createVerifyRoutes = createVerifyRoutes;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const verifySolTransfer_js_1 = require("../utils/verifySolTransfer.js");
|
|
6
|
+
function createVerifyRoutes(solanaService) {
|
|
7
|
+
const router = (0, express_1.Router)();
|
|
8
|
+
router.post('/verify/sol-transfer', async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const body = req.body;
|
|
11
|
+
if (!body || typeof body.signature !== 'string') {
|
|
12
|
+
return res.status(400).json({ error: 'Missing signature' });
|
|
13
|
+
}
|
|
14
|
+
if (typeof body.expectedDestination !== 'string' || body.expectedDestination.length === 0) {
|
|
15
|
+
return res.status(400).json({ error: 'Missing expectedDestination' });
|
|
16
|
+
}
|
|
17
|
+
if (body.expectedSource !== undefined && typeof body.expectedSource !== 'string') {
|
|
18
|
+
return res.status(400).json({ error: 'Invalid expectedSource' });
|
|
19
|
+
}
|
|
20
|
+
if (body.minLamports !== undefined && typeof body.minLamports !== 'number') {
|
|
21
|
+
return res.status(400).json({ error: 'Invalid minLamports' });
|
|
22
|
+
}
|
|
23
|
+
const connection = solanaService.getConnection();
|
|
24
|
+
const tx = await connection.getParsedTransaction(body.signature, {
|
|
25
|
+
commitment: 'finalized',
|
|
26
|
+
maxSupportedTransactionVersion: 0,
|
|
27
|
+
});
|
|
28
|
+
const result = (0, verifySolTransfer_js_1.verifySolTransferFromParsedTransaction)(tx, {
|
|
29
|
+
signature: body.signature,
|
|
30
|
+
expectedSource: body.expectedSource,
|
|
31
|
+
expectedDestination: body.expectedDestination,
|
|
32
|
+
minLamports: body.minLamports,
|
|
33
|
+
});
|
|
34
|
+
return res.json({
|
|
35
|
+
ok: true,
|
|
36
|
+
signature: result.signature,
|
|
37
|
+
observedLamports: result.observedLamports,
|
|
38
|
+
source: result.source,
|
|
39
|
+
destination: result.destination,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : 'Verification failed';
|
|
44
|
+
return res.status(400).json({ error: message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return router;
|
|
48
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Withdrawal endpoints for withdrawing from user's Privacy Cash to company wallet
|
|
3
|
+
*
|
|
4
|
+
* POST /withdraw - Withdraw from a user's Privacy Cash account to company wallet
|
|
5
|
+
* POST /withdraw/balance - Get a user's private balance in Privacy Cash
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Withdrawal requires user's keypair (reconstructed from stored shares)
|
|
9
|
+
* - Funds go from user's Privacy Cash account to company wallet
|
|
10
|
+
* - This is the second half of the privacy flow (deposit → wait → withdraw)
|
|
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 createWithdrawRoutes(privacyCash: PrivacyCashService, _jupiter?: JupiterService): Router;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Withdrawal endpoints for withdrawing from user's Privacy Cash to company wallet
|
|
4
|
+
*
|
|
5
|
+
* POST /withdraw - Withdraw from a user's Privacy Cash account to company wallet
|
|
6
|
+
* POST /withdraw/balance - Get a user's private balance in Privacy Cash
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* - Withdrawal requires user's keypair (reconstructed from stored shares)
|
|
10
|
+
* - Funds go from user's Privacy Cash account to company wallet
|
|
11
|
+
* - This is the second half of the privacy flow (deposit → wait → withdraw)
|
|
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.createWithdrawRoutes = createWithdrawRoutes;
|
|
18
|
+
const express_1 = require("express");
|
|
19
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
20
|
+
const bs58_1 = __importDefault(require("bs58"));
|
|
21
|
+
function createWithdrawRoutes(privacyCash, _jupiter) {
|
|
22
|
+
const router = (0, express_1.Router)();
|
|
23
|
+
/**
|
|
24
|
+
* POST /withdraw
|
|
25
|
+
*
|
|
26
|
+
* Withdraw funds from a user's Privacy Cash account to the company wallet.
|
|
27
|
+
* This is called after the "privacy period" has elapsed.
|
|
28
|
+
*
|
|
29
|
+
* The user's keypair is reconstructed from stored SSS shares (Share A + Share B).
|
|
30
|
+
* Funds are sent to the configured company wallet address.
|
|
31
|
+
*/
|
|
32
|
+
router.post('/withdraw', async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const body = req.body;
|
|
35
|
+
// Validate request
|
|
36
|
+
if (!body.user_private_key || typeof body.user_private_key !== 'string') {
|
|
37
|
+
res.status(400).json({ error: 'user_private_key is required and must be a base58 string' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!body.amount_lamports || typeof body.amount_lamports !== 'number') {
|
|
41
|
+
res.status(400).json({ error: 'amount_lamports is required and must be a number' });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (body.amount_lamports <= 0) {
|
|
45
|
+
res.status(400).json({ error: 'amount_lamports must be positive' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Decode the private key
|
|
49
|
+
let userKeypair;
|
|
50
|
+
try {
|
|
51
|
+
const privateKeyBytes = bs58_1.default.decode(body.user_private_key);
|
|
52
|
+
userKeypair = web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
res.status(400).json({ error: 'Invalid private key format' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Execute the withdrawal from user's account to company wallet
|
|
59
|
+
const result = await privacyCash.withdrawFromUser(userKeypair, body.amount_lamports);
|
|
60
|
+
// NOTE: Swap-on-withdraw is not supported.
|
|
61
|
+
// The Privacy Cash withdrawal sends funds to the company wallet.
|
|
62
|
+
// Swapping would require signing with the company/treasury key.
|
|
63
|
+
const targetCurrency = body.target_currency?.toUpperCase() || 'SOL';
|
|
64
|
+
if (targetCurrency !== 'SOL') {
|
|
65
|
+
res.json({
|
|
66
|
+
success: result.success,
|
|
67
|
+
tx_signature: result.txSignature,
|
|
68
|
+
fee_lamports: result.feeLamports,
|
|
69
|
+
amount_lamports: result.amountLamports,
|
|
70
|
+
is_partial: result.isPartial,
|
|
71
|
+
swap_failed: true,
|
|
72
|
+
swap_error: 'Swap-on-withdraw is not supported; funds remain in SOL',
|
|
73
|
+
currency: 'SOL',
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// No swap needed - return SOL
|
|
78
|
+
res.json({
|
|
79
|
+
success: result.success,
|
|
80
|
+
tx_signature: result.txSignature,
|
|
81
|
+
fee_lamports: result.feeLamports,
|
|
82
|
+
amount_lamports: result.amountLamports,
|
|
83
|
+
is_partial: result.isPartial,
|
|
84
|
+
currency: 'SOL',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error('[Withdraw] Error:', error);
|
|
89
|
+
res.status(500).json({
|
|
90
|
+
error: 'Failed to withdraw funds',
|
|
91
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* POST /withdraw/balance
|
|
97
|
+
*
|
|
98
|
+
* Get a user's private balance in Privacy Cash.
|
|
99
|
+
* Requires the user's keypair to decrypt UTXO data.
|
|
100
|
+
*
|
|
101
|
+
* Note: This is POST (not GET) because it requires a request body with the private key.
|
|
102
|
+
*/
|
|
103
|
+
router.post('/withdraw/balance', async (req, res) => {
|
|
104
|
+
try {
|
|
105
|
+
const body = req.body;
|
|
106
|
+
// Validate request
|
|
107
|
+
if (!body.user_private_key || typeof body.user_private_key !== 'string') {
|
|
108
|
+
res.status(400).json({ error: 'user_private_key is required and must be a base58 string' });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Decode the private key
|
|
112
|
+
let userKeypair;
|
|
113
|
+
try {
|
|
114
|
+
const privateKeyBytes = bs58_1.default.decode(body.user_private_key);
|
|
115
|
+
userKeypair = web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
res.status(400).json({ error: 'Invalid private key format' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const balanceLamports = await privacyCash.getUserPrivateBalance(userKeypair);
|
|
122
|
+
res.json({
|
|
123
|
+
balance_lamports: balanceLamports,
|
|
124
|
+
balance_sol: balanceLamports / 1_000_000_000,
|
|
125
|
+
user_pubkey: userKeypair.publicKey.toBase58(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
console.error('[Withdraw/Balance] Error:', error);
|
|
130
|
+
res.status(500).json({
|
|
131
|
+
error: 'Failed to get private balance',
|
|
132
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return router;
|
|
137
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jupiter Ultra API service for gasless SPL token → SOL swaps
|
|
3
|
+
*
|
|
4
|
+
* Jupiter Ultra provides gasless swaps when:
|
|
5
|
+
* - User wallet has < 0.01 SOL (embedded wallets qualify)
|
|
6
|
+
* - Trade size > ~$10 USD (configured via JUPITER_MIN_SWAP_USD)
|
|
7
|
+
* - Jupiter pays transaction fees, deducted from swap output
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. GET /order - Get unsigned swap transaction
|
|
11
|
+
* 2. Sign transaction with user keypair
|
|
12
|
+
* 3. POST /execute - Submit signed transaction (Jupiter pays gas)
|
|
13
|
+
*/
|
|
14
|
+
import { Keypair } from '@solana/web3.js';
|
|
15
|
+
import { Config } from '../config.js';
|
|
16
|
+
/** USDC token mint address on Solana mainnet */
|
|
17
|
+
export declare const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
18
|
+
/** USDT token mint address on Solana mainnet */
|
|
19
|
+
export declare const USDT_MINT = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB";
|
|
20
|
+
/** Parameters for getting a swap order */
|
|
21
|
+
export interface SwapOrderParams {
|
|
22
|
+
/** SPL token mint address to swap from (e.g., USDC) */
|
|
23
|
+
inputMint: string;
|
|
24
|
+
/** Amount in token's smallest unit (e.g., 10 USDC = 10_000_000 for 6 decimals) */
|
|
25
|
+
amount: string;
|
|
26
|
+
/** User's wallet address (taker) */
|
|
27
|
+
takerAddress: string;
|
|
28
|
+
}
|
|
29
|
+
/** Result of getting a swap order */
|
|
30
|
+
export interface SwapOrder {
|
|
31
|
+
/** Base64 encoded unsigned transaction */
|
|
32
|
+
transaction: string;
|
|
33
|
+
/** Request ID for tracking */
|
|
34
|
+
requestId: string;
|
|
35
|
+
/** Expected output amount in smallest unit */
|
|
36
|
+
expectedOutAmount: string;
|
|
37
|
+
/** Price impact percentage */
|
|
38
|
+
priceImpactPct: string;
|
|
39
|
+
/** Whether this order qualifies for gasless execution */
|
|
40
|
+
gasless: boolean;
|
|
41
|
+
/** Slippage in basis points */
|
|
42
|
+
slippageBps: number;
|
|
43
|
+
}
|
|
44
|
+
/** Result of executing a swap */
|
|
45
|
+
export interface SwapResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
/** Transaction signature */
|
|
48
|
+
txSignature: string;
|
|
49
|
+
/** Expected output amount (from order) */
|
|
50
|
+
expectedOutAmount: string;
|
|
51
|
+
/** Actual output amount received (if available) */
|
|
52
|
+
actualOutAmount?: string;
|
|
53
|
+
/** Whether gasless was used */
|
|
54
|
+
gasless: boolean;
|
|
55
|
+
/** Error message if failed */
|
|
56
|
+
error?: string;
|
|
57
|
+
/** Error code if failed */
|
|
58
|
+
errorCode?: number;
|
|
59
|
+
}
|
|
60
|
+
export declare class JupiterService {
|
|
61
|
+
private apiUrl;
|
|
62
|
+
private apiKey;
|
|
63
|
+
private minSwapUsd;
|
|
64
|
+
private rateLimit;
|
|
65
|
+
constructor(config: Config);
|
|
66
|
+
/**
|
|
67
|
+
* Get minimum swap amount in USD required for gasless swaps
|
|
68
|
+
*/
|
|
69
|
+
getMinSwapUsd(): number;
|
|
70
|
+
/**
|
|
71
|
+
* Get configured rate limit (requests per 10 seconds) for queue throttling
|
|
72
|
+
*/
|
|
73
|
+
getRateLimit(): number;
|
|
74
|
+
/**
|
|
75
|
+
* Get a swap order (unsigned transaction) from Jupiter Ultra API
|
|
76
|
+
*
|
|
77
|
+
* @param params - Swap parameters (inputMint, amount, takerAddress)
|
|
78
|
+
* @returns SwapOrder with unsigned transaction and expected output
|
|
79
|
+
* @throws Error if Jupiter API returns an error or network fails
|
|
80
|
+
*/
|
|
81
|
+
getSwapOrder(params: SwapOrderParams): Promise<SwapOrder>;
|
|
82
|
+
/**
|
|
83
|
+
* Sign and execute a swap transaction
|
|
84
|
+
*
|
|
85
|
+
* Takes an unsigned transaction from getSwapOrder(), signs it with the user's
|
|
86
|
+
* keypair, and submits it to Jupiter for execution (gasless).
|
|
87
|
+
*
|
|
88
|
+
* @param order - SwapOrder from getSwapOrder()
|
|
89
|
+
* @param userKeypair - User's keypair to sign the transaction
|
|
90
|
+
* @returns SwapResult with transaction signature
|
|
91
|
+
* @throws Error if signing fails or Jupiter execution fails
|
|
92
|
+
*/
|
|
93
|
+
executeSwap(order: SwapOrder, userKeypair: Keypair): Promise<SwapResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Perform a complete gasless swap from SPL token to SOL
|
|
96
|
+
*
|
|
97
|
+
* Combines getSwapOrder() and executeSwap() into a single call.
|
|
98
|
+
* This is the main method to use for swapping tokens.
|
|
99
|
+
*
|
|
100
|
+
* @param inputMint - SPL token mint address to swap from
|
|
101
|
+
* @param amount - Amount in token's smallest unit
|
|
102
|
+
* @param userKeypair - User's keypair (for signing and as taker address)
|
|
103
|
+
* @returns SwapResult with transaction signature and output amount
|
|
104
|
+
*/
|
|
105
|
+
swapToSol(inputMint: string, amount: string, userKeypair: Keypair): Promise<SwapResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Perform a complete swap from SOL to SPL token (for withdrawals)
|
|
108
|
+
*
|
|
109
|
+
* This is used when the company's preferred currency is not SOL.
|
|
110
|
+
* The SOL from Privacy Cash is swapped to USDC/USDT via Jupiter.
|
|
111
|
+
*
|
|
112
|
+
* @param outputMint - SPL token mint address to swap to (e.g., USDC, USDT)
|
|
113
|
+
* @param amountLamports - Amount of SOL in lamports to swap
|
|
114
|
+
* @param userKeypair - User's keypair (for signing and as taker address)
|
|
115
|
+
* @returns SwapResult with transaction signature and output amount
|
|
116
|
+
*/
|
|
117
|
+
swapFromSol(outputMint: string, amountLamports: string, userKeypair: Keypair): Promise<SwapResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Get the mint address for a currency code
|
|
120
|
+
*
|
|
121
|
+
* @param currency - Currency code (SOL, USDC, USDT)
|
|
122
|
+
* @returns Mint address or null if unsupported/is SOL
|
|
123
|
+
*/
|
|
124
|
+
static getMintForCurrency(currency: string): string | null;
|
|
125
|
+
/**
|
|
126
|
+
* Parse a base58-encoded private key into a Keypair
|
|
127
|
+
*/
|
|
128
|
+
static parseKeypair(privateKeyBase58: string): Keypair;
|
|
129
|
+
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Jupiter Ultra API service for gasless SPL token → SOL swaps
|
|
4
|
+
*
|
|
5
|
+
* Jupiter Ultra provides gasless swaps when:
|
|
6
|
+
* - User wallet has < 0.01 SOL (embedded wallets qualify)
|
|
7
|
+
* - Trade size > ~$10 USD (configured via JUPITER_MIN_SWAP_USD)
|
|
8
|
+
* - Jupiter pays transaction fees, deducted from swap output
|
|
9
|
+
*
|
|
10
|
+
* Flow:
|
|
11
|
+
* 1. GET /order - Get unsigned swap transaction
|
|
12
|
+
* 2. Sign transaction with user keypair
|
|
13
|
+
* 3. POST /execute - Submit signed transaction (Jupiter pays gas)
|
|
14
|
+
*/
|
|
15
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
16
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
17
|
+
};
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.JupiterService = exports.USDT_MINT = exports.USDC_MINT = void 0;
|
|
20
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
21
|
+
const bs58_1 = __importDefault(require("bs58"));
|
|
22
|
+
const fetchWithTimeout_js_1 = require("../utils/fetchWithTimeout.js");
|
|
23
|
+
/** SOL token mint address (native SOL wrapped) */
|
|
24
|
+
const SOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
25
|
+
/** Default Jupiter HTTP timeout (ms) */
|
|
26
|
+
const JUPITER_TIMEOUT_MS = 10_000;
|
|
27
|
+
/** USDC token mint address on Solana mainnet */
|
|
28
|
+
exports.USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
|
|
29
|
+
/** USDT token mint address on Solana mainnet */
|
|
30
|
+
exports.USDT_MINT = 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB';
|
|
31
|
+
class JupiterService {
|
|
32
|
+
apiUrl;
|
|
33
|
+
apiKey;
|
|
34
|
+
minSwapUsd;
|
|
35
|
+
rateLimit;
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.apiUrl = config.jupiter.apiUrl;
|
|
38
|
+
this.apiKey = config.jupiter.apiKey;
|
|
39
|
+
this.minSwapUsd = config.jupiter.minSwapUsd;
|
|
40
|
+
this.rateLimit = config.jupiter.rateLimit;
|
|
41
|
+
if (!this.apiKey) {
|
|
42
|
+
console.warn('[Jupiter] WARNING: No API key configured. Jupiter Ultra API requires an API key. Get one free at https://portal.jup.ag');
|
|
43
|
+
}
|
|
44
|
+
console.log(`[Jupiter] Initialized with API: ${this.apiUrl}, minSwapUsd: ${this.minSwapUsd}, rateLimit: ${this.rateLimit}/10s`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get minimum swap amount in USD required for gasless swaps
|
|
48
|
+
*/
|
|
49
|
+
getMinSwapUsd() {
|
|
50
|
+
return this.minSwapUsd;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get configured rate limit (requests per 10 seconds) for queue throttling
|
|
54
|
+
*/
|
|
55
|
+
getRateLimit() {
|
|
56
|
+
return this.rateLimit;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get a swap order (unsigned transaction) from Jupiter Ultra API
|
|
60
|
+
*
|
|
61
|
+
* @param params - Swap parameters (inputMint, amount, takerAddress)
|
|
62
|
+
* @returns SwapOrder with unsigned transaction and expected output
|
|
63
|
+
* @throws Error if Jupiter API returns an error or network fails
|
|
64
|
+
*/
|
|
65
|
+
async getSwapOrder(params) {
|
|
66
|
+
const { inputMint, amount, takerAddress } = params;
|
|
67
|
+
// Build query parameters
|
|
68
|
+
const queryParams = new URLSearchParams({
|
|
69
|
+
inputMint,
|
|
70
|
+
outputMint: SOL_MINT,
|
|
71
|
+
amount,
|
|
72
|
+
taker: takerAddress,
|
|
73
|
+
});
|
|
74
|
+
const url = `${this.apiUrl}/order?${queryParams.toString()}`;
|
|
75
|
+
const headers = {
|
|
76
|
+
'Accept': 'application/json',
|
|
77
|
+
};
|
|
78
|
+
// Add API key if configured (required for production)
|
|
79
|
+
if (this.apiKey) {
|
|
80
|
+
headers['x-api-key'] = this.apiKey;
|
|
81
|
+
}
|
|
82
|
+
console.log(`[Jupiter] Getting swap order: ${inputMint} -> SOL, amount: ${amount}`);
|
|
83
|
+
const response = await (0, fetchWithTimeout_js_1.fetchWithTimeout)(fetch, url, { headers }, JUPITER_TIMEOUT_MS);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const errorBody = await response.text();
|
|
86
|
+
let errorMessage = `Jupiter API error: ${response.status}`;
|
|
87
|
+
try {
|
|
88
|
+
const errorJson = JSON.parse(errorBody);
|
|
89
|
+
errorMessage = errorJson.error || errorMessage;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
errorMessage = errorBody || errorMessage;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(errorMessage);
|
|
95
|
+
}
|
|
96
|
+
const order = await response.json();
|
|
97
|
+
// Check for order-level errors (codes 1, 2, 3)
|
|
98
|
+
if (order.error || order.code) {
|
|
99
|
+
const errorMessages = {
|
|
100
|
+
1: 'Insufficient token balance for swap',
|
|
101
|
+
2: 'Insufficient SOL for gas fees',
|
|
102
|
+
3: 'Trade size below minimum for gasless swap',
|
|
103
|
+
};
|
|
104
|
+
const errorMsg = order.error || errorMessages[order.code] || `Order failed with code ${order.code}`;
|
|
105
|
+
throw new Error(errorMsg);
|
|
106
|
+
}
|
|
107
|
+
// Validate required fields are present
|
|
108
|
+
if (!order.transaction || !order.requestId || !order.outAmount) {
|
|
109
|
+
throw new Error('Invalid order response: missing required fields');
|
|
110
|
+
}
|
|
111
|
+
console.log(`[Jupiter] Order received: requestId=${order.requestId}, outAmount=${order.outAmount}, gasless=${order.gasless}, router=${order.router}`);
|
|
112
|
+
return {
|
|
113
|
+
transaction: order.transaction,
|
|
114
|
+
requestId: order.requestId,
|
|
115
|
+
expectedOutAmount: order.outAmount,
|
|
116
|
+
priceImpactPct: order.priceImpactPct || '0',
|
|
117
|
+
gasless: order.gasless ?? false,
|
|
118
|
+
slippageBps: order.slippageBps ?? 0,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Sign and execute a swap transaction
|
|
123
|
+
*
|
|
124
|
+
* Takes an unsigned transaction from getSwapOrder(), signs it with the user's
|
|
125
|
+
* keypair, and submits it to Jupiter for execution (gasless).
|
|
126
|
+
*
|
|
127
|
+
* @param order - SwapOrder from getSwapOrder()
|
|
128
|
+
* @param userKeypair - User's keypair to sign the transaction
|
|
129
|
+
* @returns SwapResult with transaction signature
|
|
130
|
+
* @throws Error if signing fails or Jupiter execution fails
|
|
131
|
+
*/
|
|
132
|
+
async executeSwap(order, userKeypair) {
|
|
133
|
+
console.log(`[Jupiter] Executing swap: requestId=${order.requestId}, gasless=${order.gasless}`);
|
|
134
|
+
// Deserialize the transaction from base64
|
|
135
|
+
const txBuffer = Buffer.from(order.transaction, 'base64');
|
|
136
|
+
const transaction = web3_js_1.VersionedTransaction.deserialize(txBuffer);
|
|
137
|
+
// Sign the transaction with user's keypair
|
|
138
|
+
transaction.sign([userKeypair]);
|
|
139
|
+
// Serialize the signed transaction back to base64
|
|
140
|
+
const signedTxBase64 = Buffer.from(transaction.serialize()).toString('base64');
|
|
141
|
+
// Submit to Jupiter execute endpoint
|
|
142
|
+
const headers = {
|
|
143
|
+
'Content-Type': 'application/json',
|
|
144
|
+
'Accept': 'application/json',
|
|
145
|
+
};
|
|
146
|
+
if (this.apiKey) {
|
|
147
|
+
headers['x-api-key'] = this.apiKey;
|
|
148
|
+
}
|
|
149
|
+
const response = await (0, fetchWithTimeout_js_1.fetchWithTimeout)(fetch, `${this.apiUrl}/execute`, {
|
|
150
|
+
method: 'POST',
|
|
151
|
+
headers,
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
signedTransaction: signedTxBase64,
|
|
154
|
+
requestId: order.requestId,
|
|
155
|
+
}),
|
|
156
|
+
}, JUPITER_TIMEOUT_MS);
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
const errorBody = await response.text();
|
|
159
|
+
let errorMessage = `Jupiter execute error: ${response.status}`;
|
|
160
|
+
try {
|
|
161
|
+
const errorJson = JSON.parse(errorBody);
|
|
162
|
+
errorMessage = errorJson.error || errorMessage;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
errorMessage = errorBody || errorMessage;
|
|
166
|
+
}
|
|
167
|
+
throw new Error(errorMessage);
|
|
168
|
+
}
|
|
169
|
+
const result = await response.json();
|
|
170
|
+
// Check for failure via status or error code
|
|
171
|
+
if (result.status !== 'Success' || (result.code !== undefined && result.code !== 0)) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
txSignature: result.signature || '',
|
|
175
|
+
expectedOutAmount: order.expectedOutAmount,
|
|
176
|
+
gasless: order.gasless,
|
|
177
|
+
error: result.error || `Swap failed with code ${result.code}`,
|
|
178
|
+
errorCode: result.code,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
console.log(`[Jupiter] Swap executed: signature=${result.signature}, actualOut=${result.outputAmountResult || 'N/A'}`);
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
txSignature: result.signature,
|
|
185
|
+
expectedOutAmount: order.expectedOutAmount,
|
|
186
|
+
actualOutAmount: result.outputAmountResult,
|
|
187
|
+
gasless: order.gasless,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Perform a complete gasless swap from SPL token to SOL
|
|
192
|
+
*
|
|
193
|
+
* Combines getSwapOrder() and executeSwap() into a single call.
|
|
194
|
+
* This is the main method to use for swapping tokens.
|
|
195
|
+
*
|
|
196
|
+
* @param inputMint - SPL token mint address to swap from
|
|
197
|
+
* @param amount - Amount in token's smallest unit
|
|
198
|
+
* @param userKeypair - User's keypair (for signing and as taker address)
|
|
199
|
+
* @returns SwapResult with transaction signature and output amount
|
|
200
|
+
*/
|
|
201
|
+
async swapToSol(inputMint, amount, userKeypair) {
|
|
202
|
+
const takerAddress = userKeypair.publicKey.toBase58();
|
|
203
|
+
// Get the swap order (unsigned transaction)
|
|
204
|
+
const order = await this.getSwapOrder({
|
|
205
|
+
inputMint,
|
|
206
|
+
amount,
|
|
207
|
+
takerAddress,
|
|
208
|
+
});
|
|
209
|
+
// Sign and execute the swap
|
|
210
|
+
return this.executeSwap(order, userKeypair);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Perform a complete swap from SOL to SPL token (for withdrawals)
|
|
214
|
+
*
|
|
215
|
+
* This is used when the company's preferred currency is not SOL.
|
|
216
|
+
* The SOL from Privacy Cash is swapped to USDC/USDT via Jupiter.
|
|
217
|
+
*
|
|
218
|
+
* @param outputMint - SPL token mint address to swap to (e.g., USDC, USDT)
|
|
219
|
+
* @param amountLamports - Amount of SOL in lamports to swap
|
|
220
|
+
* @param userKeypair - User's keypair (for signing and as taker address)
|
|
221
|
+
* @returns SwapResult with transaction signature and output amount
|
|
222
|
+
*/
|
|
223
|
+
async swapFromSol(outputMint, amountLamports, userKeypair) {
|
|
224
|
+
const takerAddress = userKeypair.publicKey.toBase58();
|
|
225
|
+
// Build query parameters for SOL -> outputMint
|
|
226
|
+
const queryParams = new URLSearchParams({
|
|
227
|
+
inputMint: SOL_MINT,
|
|
228
|
+
outputMint,
|
|
229
|
+
amount: amountLamports,
|
|
230
|
+
taker: takerAddress,
|
|
231
|
+
});
|
|
232
|
+
const url = `${this.apiUrl}/order?${queryParams.toString()}`;
|
|
233
|
+
const headers = {
|
|
234
|
+
'Accept': 'application/json',
|
|
235
|
+
};
|
|
236
|
+
if (this.apiKey) {
|
|
237
|
+
headers['x-api-key'] = this.apiKey;
|
|
238
|
+
}
|
|
239
|
+
console.log(`[Jupiter] Getting swap order: SOL -> ${outputMint}, amount: ${amountLamports} lamports`);
|
|
240
|
+
const response = await fetch(url, { headers });
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
const errorBody = await response.text();
|
|
243
|
+
let errorMessage = `Jupiter API error: ${response.status}`;
|
|
244
|
+
try {
|
|
245
|
+
const errorJson = JSON.parse(errorBody);
|
|
246
|
+
errorMessage = errorJson.error || errorMessage;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
errorMessage = errorBody || errorMessage;
|
|
250
|
+
}
|
|
251
|
+
throw new Error(errorMessage);
|
|
252
|
+
}
|
|
253
|
+
const order = await response.json();
|
|
254
|
+
// Check for order-level errors (codes 1, 2, 3)
|
|
255
|
+
if (order.error || order.code) {
|
|
256
|
+
const errorMessages = {
|
|
257
|
+
1: 'Insufficient token balance for swap',
|
|
258
|
+
2: 'Insufficient SOL for gas fees',
|
|
259
|
+
3: 'Trade size below minimum for gasless swap',
|
|
260
|
+
};
|
|
261
|
+
const errorMsg = order.error || errorMessages[order.code] || `Order failed with code ${order.code}`;
|
|
262
|
+
throw new Error(errorMsg);
|
|
263
|
+
}
|
|
264
|
+
// Validate required fields are present
|
|
265
|
+
if (!order.transaction || !order.requestId || !order.outAmount) {
|
|
266
|
+
throw new Error('Invalid order response: missing required fields');
|
|
267
|
+
}
|
|
268
|
+
console.log(`[Jupiter] Order received: requestId=${order.requestId}, outAmount=${order.outAmount}, gasless=${order.gasless}, router=${order.router}`);
|
|
269
|
+
// Execute the swap
|
|
270
|
+
return this.executeSwap({
|
|
271
|
+
transaction: order.transaction,
|
|
272
|
+
requestId: order.requestId,
|
|
273
|
+
expectedOutAmount: order.outAmount,
|
|
274
|
+
priceImpactPct: order.priceImpactPct || '0',
|
|
275
|
+
gasless: order.gasless ?? false,
|
|
276
|
+
slippageBps: order.slippageBps ?? 0,
|
|
277
|
+
}, userKeypair);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Get the mint address for a currency code
|
|
281
|
+
*
|
|
282
|
+
* @param currency - Currency code (SOL, USDC, USDT)
|
|
283
|
+
* @returns Mint address or null if unsupported/is SOL
|
|
284
|
+
*/
|
|
285
|
+
static getMintForCurrency(currency) {
|
|
286
|
+
switch (currency.toUpperCase()) {
|
|
287
|
+
case 'USDC':
|
|
288
|
+
return exports.USDC_MINT;
|
|
289
|
+
case 'USDT':
|
|
290
|
+
return exports.USDT_MINT;
|
|
291
|
+
case 'SOL':
|
|
292
|
+
default:
|
|
293
|
+
return null; // No swap needed for SOL
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Parse a base58-encoded private key into a Keypair
|
|
298
|
+
*/
|
|
299
|
+
static parseKeypair(privateKeyBase58) {
|
|
300
|
+
const privateKeyBytes = bs58_1.default.decode(privateKeyBase58);
|
|
301
|
+
return web3_js_1.Keypair.fromSecretKey(privateKeyBytes);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.JupiterService = JupiterService;
|