initlast
This commit is contained in:
@@ -21,7 +21,13 @@ import { derivePath } from 'ed25519-hd-key';
|
||||
import { env } from '../config/env';
|
||||
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
||||
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
||||
import {
|
||||
proxiedFetch,
|
||||
pickProxiedEvmProvider,
|
||||
getProxiedSolConnection,
|
||||
} from '../lib/outbound-proxy';
|
||||
import { logger } from '../lib/logger';
|
||||
import { getSolTokens } from '../lib/token-registry';
|
||||
|
||||
const HTTP_TIMEOUT_MS = 20_000;
|
||||
const MAX_GAS_PRICE_GWEI = 500;
|
||||
@@ -68,21 +74,10 @@ export interface SwapBscParams {
|
||||
feeTier?: FeeTier;
|
||||
}
|
||||
|
||||
/** Wrapper над `pickProxiedEvmProvider` — все swap-orchestrator EVM calls
|
||||
* идут через OUTBOUND_PROXY_URL если задан. Если не задан — fallback direct. */
|
||||
async function pickProvider(rpcs: string[], chainId: number): Promise<ethers.providers.StaticJsonRpcProvider> {
|
||||
let lastErr: any;
|
||||
for (const url of rpcs) {
|
||||
const p = new ethers.providers.StaticJsonRpcProvider(url, chainId);
|
||||
try {
|
||||
await Promise.race([
|
||||
p.getBlockNumber(),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('rpc_alive_timeout')), 3000)),
|
||||
]);
|
||||
return p;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw new Error(`All BSC RPCs failed: ${lastErr?.message || lastErr}`);
|
||||
return pickProxiedEvmProvider(rpcs, chainId);
|
||||
}
|
||||
|
||||
function withTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
|
||||
@@ -295,7 +290,8 @@ async function fetchJson(url: string, init?: RequestInit): Promise<any> {
|
||||
const controller = new AbortController();
|
||||
const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
||||
try {
|
||||
const res = await fetch(url, { ...init, signal: controller.signal });
|
||||
// proxiedFetch routes через OUTBOUND_PROXY_URL если задан, иначе native fetch.
|
||||
const res = await proxiedFetch(url, { ...init, signal: controller.signal });
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
throw new Error(`Upstream ${res.status}: ${body.slice(0, 200)}`);
|
||||
@@ -750,10 +746,28 @@ const SOL_RPC = 'https://api.mainnet-beta.solana.com';
|
||||
// `lite-api.jup.ag/swap/v1` — public anonymous endpoint (~600 req/min), JSON-schema идентична.
|
||||
const JUPITER_API = 'https://lite-api.jup.ag/swap/v1';
|
||||
|
||||
// SOL mint whitelist для custodial swap. Соответствует sol-swap-proxy.ALLOWED_MINTS.
|
||||
// Включает wrapped SOL (для Jupiter native обозначение) + 14 SPL tokens из token-registry.
|
||||
// Lazy-init из getSolTokens() чтобы registry оставался single source of truth.
|
||||
const SOL_NATIVE_WRAPPED_MINT = 'So11111111111111111111111111111111111111112';
|
||||
let _solMintWhitelist: Set<string> | null = null;
|
||||
function isAllowedSolMint(mint: string): boolean {
|
||||
if (!_solMintWhitelist) {
|
||||
_solMintWhitelist = new Set([
|
||||
SOL_NATIVE_WRAPPED_MINT,
|
||||
...getSolTokens().map((t) => t.mint),
|
||||
]);
|
||||
}
|
||||
return _solMintWhitelist.has(mint);
|
||||
}
|
||||
|
||||
let _solConnection: Connection | null = null;
|
||||
function getSolConnection(): Connection {
|
||||
if (!_solConnection) {
|
||||
_solConnection = new Connection(SOL_RPC, 'confirmed');
|
||||
// Через OUTBOUND_PROXY_URL если задан (swap path) — Jupiter swap broadcast'ы идут
|
||||
// через proxy. Если env пустой — proxiedFetch fallback'ит на native, Connection
|
||||
// работает direct.
|
||||
_solConnection = getProxiedSolConnection(SOL_RPC, 'confirmed');
|
||||
}
|
||||
return _solConnection;
|
||||
}
|
||||
@@ -786,6 +800,19 @@ export async function swapSol(p: SwapSolParams): Promise<{ signature: string }>
|
||||
throw new Error('slippageBps must be 1-1000');
|
||||
}
|
||||
|
||||
// Mint whitelist — соответствует sol-swap-proxy.ALLOWED_MINTS + token-registry.SOL_TOKENS.
|
||||
// Без этого custodial endpoint позволил бы swap'ать произвольные SPL mints (rugpull tokens,
|
||||
// honeypots) — клиент мог бы дренить wallet через malicious mint quote.
|
||||
if (!isAllowedSolMint(p.inputMint)) {
|
||||
throw new Error(`SOL swap inputMint not in whitelist: ${p.inputMint}`);
|
||||
}
|
||||
if (!isAllowedSolMint(p.outputMint)) {
|
||||
throw new Error(`SOL swap outputMint not in whitelist: ${p.outputMint}`);
|
||||
}
|
||||
if (p.inputMint === p.outputMint) {
|
||||
throw new Error('SOL swap: inputMint === outputMint');
|
||||
}
|
||||
|
||||
// 1. Jupiter quote
|
||||
const quoteUrl = `${JUPITER_API}/quote?inputMint=${encodeURIComponent(p.inputMint)}&outputMint=${encodeURIComponent(p.outputMint)}&amount=${encodeURIComponent(p.amount)}&slippageBps=${slippageBps}`;
|
||||
const headers: Record<string, string> = { Accept: 'application/json' };
|
||||
|
||||
@@ -28,6 +28,12 @@ import { derivePath } from 'ed25519-hd-key';
|
||||
import { env } from '../config/env';
|
||||
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
||||
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
||||
// Bridge-only proxy helpers: signAndBroadcastRawEvm + Solana bridge sign идут через
|
||||
// OUTBOUND_PROXY_URL. sendEvm/sendSol/sendBtc/sendTrx остаются на direct connection.
|
||||
import {
|
||||
pickProxiedEvmProvider,
|
||||
getProxiedSolConnection,
|
||||
} from '../lib/outbound-proxy';
|
||||
import { getTokenInfo } from '../lib/token-registry';
|
||||
import type { ChainCode } from '../lib/address-validators';
|
||||
|
||||
@@ -148,8 +154,9 @@ export async function signAndBroadcastRawEvm(p: RawEvmSignParams): Promise<{ txi
|
||||
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.ETH);
|
||||
assertAddressMatch(wallet.address, p.expectedFromAddress, p.chain);
|
||||
|
||||
// H29 — RPC failover
|
||||
const provider = await pickProvider(rpcs, expectedChainId);
|
||||
// H29 — RPC failover. BRIDGE path → через OUTBOUND_PROXY_URL если задан (Jupiter/Relay
|
||||
// часто rate-limit'ят cloud-IP'ы). Fallback на direct если env пустой.
|
||||
const provider = await pickProxiedEvmProvider(rpcs, expectedChainId);
|
||||
const signer = wallet.connect(provider);
|
||||
|
||||
// Cap fees против rogue/poisoned quote с insanely-high maxFeePerGas.
|
||||
@@ -422,6 +429,7 @@ async function sendSol(p: SendParams): Promise<{ txid: string }> {
|
||||
}
|
||||
|
||||
// H41 — singleton SOL Connection. Reusing avoids WebSocket leak under load.
|
||||
// Используется в sendSol (basic /send) — НЕ через proxy.
|
||||
let _solConnection: Connection | null = null;
|
||||
function getSolConnection(): Connection {
|
||||
if (!_solConnection) {
|
||||
@@ -430,6 +438,16 @@ function getSolConnection(): Connection {
|
||||
return _solConnection;
|
||||
}
|
||||
|
||||
/** Bridge-side SOL connection — через OUTBOUND_PROXY_URL если задан.
|
||||
* Используется в signAndBroadcastSolanaTx / signAndBroadcastSolanaInstructions. */
|
||||
let _solConnectionBridge: Connection | null = null;
|
||||
function getBridgeSolConnection(): Connection {
|
||||
if (!_solConnectionBridge) {
|
||||
_solConnectionBridge = getProxiedSolConnection(SOL_RPC, 'confirmed');
|
||||
}
|
||||
return _solConnectionBridge;
|
||||
}
|
||||
|
||||
// ─── SOL custodial sign-and-broadcast (для Relay bridge SOL-side) ─────
|
||||
|
||||
export interface SignSolanaTxParams {
|
||||
@@ -483,7 +501,8 @@ export async function signAndBroadcastSolanaTx(p: SignSolanaTxParams): Promise<{
|
||||
|
||||
tx.sign([keypair]);
|
||||
|
||||
const conn = getSolConnection();
|
||||
// Bridge path — через OUTBOUND_PROXY_URL если задан (Relay SOL-side / Jupiter serialized).
|
||||
const conn = getBridgeSolConnection();
|
||||
const sig = await conn.sendRawTransaction(tx.serialize());
|
||||
|
||||
try {
|
||||
@@ -576,7 +595,8 @@ export async function signAndBroadcastSolanaInstructions(
|
||||
ixs.push(new TransactionInstruction({ programId, keys, data }));
|
||||
}
|
||||
|
||||
const conn = getSolConnection();
|
||||
// Bridge path — через OUTBOUND_PROXY_URL если задан (Relay SOL bridge instructions).
|
||||
const conn = getBridgeSolConnection();
|
||||
|
||||
// ─── Resolve address lookup tables через RPC ───
|
||||
const luts: AddressLookupTableAccount[] = [];
|
||||
|
||||
Reference in New Issue
Block a user