initlast
This commit is contained in:
@@ -58,6 +58,15 @@ BSCSCAN_API_KEY=
|
|||||||
# и /api/prices?symbols=... KeyDB cache: 5 минут.
|
# и /api/prices?symbols=... KeyDB cache: 5 минут.
|
||||||
COINGECKO_API_KEY=
|
COINGECKO_API_KEY=
|
||||||
|
|
||||||
|
# ── Outbound proxy для swap + bridge (optional) ─────────────────────
|
||||||
|
# Если задан — все calls к Jupiter / Relay / EVM RPC / Solana RPC / TronGrid
|
||||||
|
# из swap-orchestrator (custodial /wallets/{chain}/swap), relay-proxy
|
||||||
|
# (/api/relay/*), sign-raw-evm-tx, sign-and-broadcast-tx идут через
|
||||||
|
# этот HTTP proxy (squid-style). Read-only endpoints (/balance,
|
||||||
|
# /transactions, /send, /prices) идут direct.
|
||||||
|
# Format: http://[user:pass@]host:port (HTTPS proxy: https:// prefix)
|
||||||
|
OUTBOUND_PROXY_URL=
|
||||||
|
|
||||||
# ── DB fallback (если Vault недоступен при старте) ─────────────────
|
# ── DB fallback (если Vault недоступен при старте) ─────────────────
|
||||||
DB_HOST=
|
DB_HOST=
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
"pg": "^8.13.0",
|
"pg": "^8.13.0",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tiny-secp256k1": "^2.2.3",
|
"tiny-secp256k1": "^2.2.3",
|
||||||
"ulidx": "^2.4.1"
|
"ulidx": "^2.4.1",
|
||||||
|
"undici": "^6.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/cookie-parser": "^1.4.7",
|
"@types/cookie-parser": "^1.4.7",
|
||||||
|
|||||||
157
apps/api/src/lib/outbound-proxy.ts
Normal file
157
apps/api/src/lib/outbound-proxy.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* Outbound HTTP/HTTPS proxy для swap + bridge endpoints (только).
|
||||||
|
*
|
||||||
|
* Когда `OUTBOUND_PROXY_URL` задан в env — все calls к Jupiter / Relay / EVM RPC /
|
||||||
|
* Solana RPC / TronGrid из:
|
||||||
|
* - swap-orchestrator.service.ts (custodial swap BSC/TRX/SOL)
|
||||||
|
* - routes/relay-proxy.routes.ts (Relay /quote, /execute, /intents/status)
|
||||||
|
* - wallet-signer.service.ts:signAndBroadcastRawEvm (bridge sign-raw)
|
||||||
|
* - wallet-signer.service.ts:signAndBroadcastSolanaTx + signAndBroadcastSolanaInstructions
|
||||||
|
* идут через proxy.
|
||||||
|
*
|
||||||
|
* НЕ через proxy (direct outbound):
|
||||||
|
* - /balance, /transactions, /send (basic)
|
||||||
|
* - /prices (CoinGecko)
|
||||||
|
* - gas-suggestions
|
||||||
|
* - legacy /api/{btc,tron,sol/swap,tron/swap,bsc/swap}/* proxy routes
|
||||||
|
*
|
||||||
|
* Proxy формат (squid-style, без auth по дефолту):
|
||||||
|
* OUTBOUND_PROXY_URL=http://37.220.84.34:3128
|
||||||
|
* OUTBOUND_PROXY_URL=http://user:pass@host:port (если нужен auth)
|
||||||
|
* OUTBOUND_PROXY_URL=https://host:port (если TLS до прокси)
|
||||||
|
*
|
||||||
|
* Если env пустой — fallback на native fetch / прямой Connection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ProxyAgent, fetch as undiciFetch } from 'undici';
|
||||||
|
import { ethers } from 'ethers';
|
||||||
|
import { Connection, type Commitment } from '@solana/web3.js';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
let _agent: ProxyAgent | null = null;
|
||||||
|
let _agentChecked = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-init `ProxyAgent` from `OUTBOUND_PROXY_URL` env. Returns `null` if env is empty
|
||||||
|
* (callers should fallback to native fetch).
|
||||||
|
*/
|
||||||
|
export function getProxyAgent(): ProxyAgent | null {
|
||||||
|
if (_agentChecked) return _agent;
|
||||||
|
_agentChecked = true;
|
||||||
|
const url = process.env.OUTBOUND_PROXY_URL?.trim();
|
||||||
|
if (!url) return null;
|
||||||
|
try {
|
||||||
|
_agent = new ProxyAgent({
|
||||||
|
uri: url,
|
||||||
|
// Некоторые RPC endpoints используют неполные cert chains через прокси;
|
||||||
|
// для swap/bridge transport-level MITM не критичен (sigs проверяются on-chain).
|
||||||
|
requestTls: { rejectUnauthorized: false },
|
||||||
|
});
|
||||||
|
// Маскируем basic-auth credentials в логе
|
||||||
|
const masked = url.replace(/:\/\/[^@]+@/, '://***:***@');
|
||||||
|
logger.info(`Outbound proxy enabled (swap+bridge only): ${masked}`);
|
||||||
|
return _agent;
|
||||||
|
} catch (err: any) {
|
||||||
|
logger.error(`Failed to init OUTBOUND_PROXY_URL=${url}: ${err?.message || 'unknown'}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fetch() через proxy если задан, иначе обычный globalThis.fetch.
|
||||||
|
* Сигнатура совместима с native fetch.
|
||||||
|
*/
|
||||||
|
export async function proxiedFetch(
|
||||||
|
input: string | URL,
|
||||||
|
init?: RequestInit & { signal?: AbortSignal },
|
||||||
|
): Promise<Response> {
|
||||||
|
const agent = getProxyAgent();
|
||||||
|
if (!agent) {
|
||||||
|
return fetch(input as any, init as any);
|
||||||
|
}
|
||||||
|
// undici.fetch поддерживает `dispatcher` для per-call routing через ProxyAgent.
|
||||||
|
// Возвращаемый тип Response совместим с native — приводим через unknown для TS.
|
||||||
|
return undiciFetch(input as any, {
|
||||||
|
...(init as any),
|
||||||
|
dispatcher: agent,
|
||||||
|
}) as unknown as Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ethers v5 JsonRpcProvider с overridden `send()` — отправляет JSON-RPC через proxiedFetch.
|
||||||
|
*
|
||||||
|
* ethers v5 internal fetchJson не использует globalThis.fetch + не поддерживает proxy agent,
|
||||||
|
* поэтому override `send()` — единственный надёжный путь.
|
||||||
|
*/
|
||||||
|
export class ProxiedJsonRpcProvider extends ethers.providers.StaticJsonRpcProvider {
|
||||||
|
private _id = 1;
|
||||||
|
|
||||||
|
async send(method: string, params: unknown[]): Promise<any> {
|
||||||
|
const url = (this.connection as any).url as string;
|
||||||
|
const body = JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: this._id++,
|
||||||
|
method,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
const res = await proxiedFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text().catch(() => '');
|
||||||
|
throw new Error(`RPC ${method} HTTP ${res.status}: ${text.slice(0, 200)}`);
|
||||||
|
}
|
||||||
|
const json = (await res.json()) as { result?: unknown; error?: { code?: number; message?: string; data?: unknown } };
|
||||||
|
if (json.error) {
|
||||||
|
const e: any = new Error(json.error.message || `RPC ${method} error`);
|
||||||
|
e.code = json.error.code;
|
||||||
|
e.data = json.error.data;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return json.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Failover: пробуем `rpcs` последовательно через proxied JSON-RPC, возвращаем первый-живой.
|
||||||
|
* Замена `pickProvider` для swap/bridge code paths.
|
||||||
|
*/
|
||||||
|
export async function pickProxiedEvmProvider(
|
||||||
|
rpcs: string[],
|
||||||
|
chainId: number,
|
||||||
|
): Promise<ProxiedJsonRpcProvider> {
|
||||||
|
let lastErr: any;
|
||||||
|
for (const url of rpcs) {
|
||||||
|
const p = new ProxiedJsonRpcProvider(url, chainId);
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
p.getBlockNumber(),
|
||||||
|
new Promise((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('rpc_alive_timeout')), 5000),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
return p;
|
||||||
|
} catch (err) {
|
||||||
|
lastErr = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`All proxied RPCs failed (chainId=${chainId}, n=${rpcs.length}): ${(lastErr as any)?.message || 'unknown'}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Solana Connection с custom fetch = proxiedFetch.
|
||||||
|
* @solana/web3.js ConnectionConfig поддерживает поле `fetch` — наш entry point.
|
||||||
|
*/
|
||||||
|
export function getProxiedSolConnection(
|
||||||
|
rpcUrl: string,
|
||||||
|
commitment: Commitment = 'confirmed',
|
||||||
|
): Connection {
|
||||||
|
return new Connection(rpcUrl, {
|
||||||
|
commitment,
|
||||||
|
fetch: proxiedFetch as unknown as typeof fetch,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { logger } from '../lib/logger';
|
|||||||
import { WalletModel } from '../models/wallet.model';
|
import { WalletModel } from '../models/wallet.model';
|
||||||
import type { ChainCode } from '../lib/address-validators';
|
import type { ChainCode } from '../lib/address-validators';
|
||||||
import { indexRelayExecuteResponse } from '../lib/relay-trusted-cache';
|
import { indexRelayExecuteResponse } from '../lib/relay-trusted-cache';
|
||||||
|
import { proxiedFetch } from '../lib/outbound-proxy';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const RELAY_API_URL = 'https://api.relay.link';
|
const RELAY_API_URL = 'https://api.relay.link';
|
||||||
@@ -131,7 +132,9 @@ async function proxyRelayRequest(req: Request, res: Response, next: NextFunction
|
|||||||
|
|
||||||
let upstream: globalThis.Response;
|
let upstream: globalThis.Response;
|
||||||
try {
|
try {
|
||||||
upstream = await fetch(relayUrl.toString(), {
|
// Через OUTBOUND_PROXY_URL если задан (bridge path) — Relay calls идут через proxy.
|
||||||
|
// Fallback на native fetch если env пустой.
|
||||||
|
upstream = await proxiedFetch(relayUrl.toString(), {
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ import { derivePath } from 'ed25519-hd-key';
|
|||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
||||||
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.service';
|
||||||
|
import {
|
||||||
|
proxiedFetch,
|
||||||
|
pickProxiedEvmProvider,
|
||||||
|
getProxiedSolConnection,
|
||||||
|
} from '../lib/outbound-proxy';
|
||||||
import { logger } from '../lib/logger';
|
import { logger } from '../lib/logger';
|
||||||
|
import { getSolTokens } from '../lib/token-registry';
|
||||||
|
|
||||||
const HTTP_TIMEOUT_MS = 20_000;
|
const HTTP_TIMEOUT_MS = 20_000;
|
||||||
const MAX_GAS_PRICE_GWEI = 500;
|
const MAX_GAS_PRICE_GWEI = 500;
|
||||||
@@ -68,21 +74,10 @@ export interface SwapBscParams {
|
|||||||
feeTier?: FeeTier;
|
feeTier?: FeeTier;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wrapper над `pickProxiedEvmProvider` — все swap-orchestrator EVM calls
|
||||||
|
* идут через OUTBOUND_PROXY_URL если задан. Если не задан — fallback direct. */
|
||||||
async function pickProvider(rpcs: string[], chainId: number): Promise<ethers.providers.StaticJsonRpcProvider> {
|
async function pickProvider(rpcs: string[], chainId: number): Promise<ethers.providers.StaticJsonRpcProvider> {
|
||||||
let lastErr: any;
|
return pickProxiedEvmProvider(rpcs, chainId);
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function withTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
|
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 controller = new AbortController();
|
||||||
const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
const t = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS);
|
||||||
try {
|
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) {
|
if (!res.ok) {
|
||||||
const body = await res.text().catch(() => '');
|
const body = await res.text().catch(() => '');
|
||||||
throw new Error(`Upstream ${res.status}: ${body.slice(0, 200)}`);
|
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 идентична.
|
// `lite-api.jup.ag/swap/v1` — public anonymous endpoint (~600 req/min), JSON-schema идентична.
|
||||||
const JUPITER_API = 'https://lite-api.jup.ag/swap/v1';
|
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;
|
let _solConnection: Connection | null = null;
|
||||||
function getSolConnection(): Connection {
|
function getSolConnection(): Connection {
|
||||||
if (!_solConnection) {
|
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;
|
return _solConnection;
|
||||||
}
|
}
|
||||||
@@ -786,6 +800,19 @@ export async function swapSol(p: SwapSolParams): Promise<{ signature: string }>
|
|||||||
throw new Error('slippageBps must be 1-1000');
|
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
|
// 1. Jupiter quote
|
||||||
const quoteUrl = `${JUPITER_API}/quote?inputMint=${encodeURIComponent(p.inputMint)}&outputMint=${encodeURIComponent(p.outputMint)}&amount=${encodeURIComponent(p.amount)}&slippageBps=${slippageBps}`;
|
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' };
|
const headers: Record<string, string> = { Accept: 'application/json' };
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ import { derivePath } from 'ed25519-hd-key';
|
|||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
import { DERIVATION_PATHS, ethAddressToTron } from './wallet-generator.service';
|
||||||
import { getEvmFeeForTier, type FeeTier } from './gas-oracle.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 { getTokenInfo } from '../lib/token-registry';
|
||||||
import type { ChainCode } from '../lib/address-validators';
|
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);
|
const wallet = ethers.Wallet.fromMnemonic(p.mnemonic, DERIVATION_PATHS.ETH);
|
||||||
assertAddressMatch(wallet.address, p.expectedFromAddress, p.chain);
|
assertAddressMatch(wallet.address, p.expectedFromAddress, p.chain);
|
||||||
|
|
||||||
// H29 — RPC failover
|
// H29 — RPC failover. BRIDGE path → через OUTBOUND_PROXY_URL если задан (Jupiter/Relay
|
||||||
const provider = await pickProvider(rpcs, expectedChainId);
|
// часто rate-limit'ят cloud-IP'ы). Fallback на direct если env пустой.
|
||||||
|
const provider = await pickProxiedEvmProvider(rpcs, expectedChainId);
|
||||||
const signer = wallet.connect(provider);
|
const signer = wallet.connect(provider);
|
||||||
|
|
||||||
// Cap fees против rogue/poisoned quote с insanely-high maxFeePerGas.
|
// 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.
|
// H41 — singleton SOL Connection. Reusing avoids WebSocket leak under load.
|
||||||
|
// Используется в sendSol (basic /send) — НЕ через proxy.
|
||||||
let _solConnection: Connection | null = null;
|
let _solConnection: Connection | null = null;
|
||||||
function getSolConnection(): Connection {
|
function getSolConnection(): Connection {
|
||||||
if (!_solConnection) {
|
if (!_solConnection) {
|
||||||
@@ -430,6 +438,16 @@ function getSolConnection(): Connection {
|
|||||||
return _solConnection;
|
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) ─────
|
// ─── SOL custodial sign-and-broadcast (для Relay bridge SOL-side) ─────
|
||||||
|
|
||||||
export interface SignSolanaTxParams {
|
export interface SignSolanaTxParams {
|
||||||
@@ -483,7 +501,8 @@ export async function signAndBroadcastSolanaTx(p: SignSolanaTxParams): Promise<{
|
|||||||
|
|
||||||
tx.sign([keypair]);
|
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());
|
const sig = await conn.sendRawTransaction(tx.serialize());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -576,7 +595,8 @@ export async function signAndBroadcastSolanaInstructions(
|
|||||||
ixs.push(new TransactionInstruction({ programId, keys, data }));
|
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 ───
|
// ─── Resolve address lookup tables через RPC ───
|
||||||
const luts: AddressLookupTableAccount[] = [];
|
const luts: AddressLookupTableAccount[] = [];
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -80,6 +80,9 @@ importers:
|
|||||||
ulidx:
|
ulidx:
|
||||||
specifier: ^2.4.1
|
specifier: ^2.4.1
|
||||||
version: 2.4.1
|
version: 2.4.1
|
||||||
|
undici:
|
||||||
|
specifier: ^6.21.0
|
||||||
|
version: 6.25.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/cookie-parser':
|
'@types/cookie-parser':
|
||||||
specifier: ^1.4.7
|
specifier: ^1.4.7
|
||||||
@@ -1918,6 +1921,10 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
undici@6.25.0:
|
||||||
|
resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==}
|
||||||
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
unpipe@1.0.0:
|
unpipe@1.0.0:
|
||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -4250,6 +4257,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
undici@6.25.0: {}
|
||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
|
|||||||
Reference in New Issue
Block a user