This commit is contained in:
ZOMBIIIIIII
2026-05-14 14:41:03 +03:00
parent 53635806d6
commit e88ee3a55f
8 changed files with 415 additions and 48 deletions

View File

@@ -6,10 +6,11 @@ import { getBalance, getTransactions } from '../services/wallet-ops.service';
import { isValidAddress, isValidAmount, ChainCode } from '../lib/address-validators';
import { generateMnemonic, deriveAllAddresses, ALL_CHAINS } from '../services/wallet-generator.service';
import { encryptMnemonic, decryptMnemonic, isCryptoReady } from '../services/crypto.service';
import { signAndBroadcast, signAndBroadcastRawEvm, signAndBroadcastSolanaTx } from '../services/wallet-signer.service';
import { signAndBroadcast, signAndBroadcastRawEvm, signAndBroadcastSolanaTx, signAndBroadcastSolanaInstructions } from '../services/wallet-signer.service';
import { swapBsc, swapTrx, swapSol } from '../services/swap-orchestrator.service';
import { getEvmFeeTiers, type FeeTier } from '../services/gas-oracle.service';
import { applyEvmTxPolicy } from '../lib/evm-tx-policy';
import { getRelayTrustedAddresses } from '../lib/relay-trusted-cache';
import { acquireSendLock } from '../lib/send-lock';
import { claimIdempotency, saveIdempotencyResponse, extractIdempotencyKey } from '../lib/idempotency';
import { auditLog, auditLogStrict, completeAudit } from '../lib/audit-log';
@@ -511,9 +512,12 @@ export const WalletController = {
}
}
// C1, C2, H2 — sign-raw policy: `to` allowlist (Relay routers only) +
// selector blacklist (approve/permit etc.) + value/gas caps. Throws on violation.
// C1, C2, H2 — sign-raw policy: `to` allowlist (Relay routers — static + dynamic
// cache из /relay/execute) + selector blacklist + value/gas caps.
// Dynamic cache позволяет авто-trust'ить новые Relay router'ы которые юзер только
// что увидел через /relay/execute (TTL 30 минут, set в Redis).
try {
const dynamicTrusted = await getRelayTrustedAddresses(Number(chainId));
applyEvmTxPolicy({
chainId: Number(chainId),
to,
@@ -521,6 +525,7 @@ export const WalletController = {
value: String(value),
gas: String(gas),
maxFeePerGas: String(maxFeePerGas),
dynamicTrusted,
});
} catch (policyErr: any) {
res.status(400).json({ success: false, error: policyErr.message });
@@ -768,13 +773,33 @@ export const WalletController = {
return;
}
const { transaction } = req.body ?? {};
if (typeof transaction !== 'string' || transaction.length === 0 || transaction.length > 8192) {
res.status(400).json({ success: false, error: 'Invalid transaction (must be non-empty base64 string ≤8KB)' });
// Body может быть в одном из двух форматов:
// A) { transaction: '<base64>' } — pre-built VersionedTransaction (от Jupiter / Relay /execute если они вернули сериализованную tx)
// B) { instructions: [...], addressLookupTableAddresses?: [...] } — Relay SOL-bridge instructions (сервер сам compile'ит tx)
const { transaction, instructions, addressLookupTableAddresses } = req.body ?? {};
const hasTx = typeof transaction === 'string';
const hasIxs = Array.isArray(instructions);
if (!hasTx && !hasIxs) {
res.status(400).json({ success: false, error: 'Body must contain either {transaction:"<base64>"} or {instructions:[...]}' });
return;
}
if (!/^[A-Za-z0-9+/=]+$/.test(transaction)) {
res.status(400).json({ success: false, error: 'Invalid base64 transaction' });
if (hasTx && hasIxs) {
res.status(400).json({ success: false, error: 'Body must contain either transaction OR instructions, not both' });
return;
}
if (hasTx) {
if (transaction.length === 0 || transaction.length > 8192) {
res.status(400).json({ success: false, error: 'Invalid transaction (must be non-empty base64 string ≤8KB)' });
return;
}
if (!/^[A-Za-z0-9+/=]+$/.test(transaction)) {
res.status(400).json({ success: false, error: 'Invalid base64 transaction' });
return;
}
}
if (hasIxs && addressLookupTableAddresses !== undefined && !Array.isArray(addressLookupTableAddresses)) {
res.status(400).json({ success: false, error: 'addressLookupTableAddresses must be an array of strings' });
return;
}
@@ -812,7 +837,9 @@ export const WalletController = {
event: 'wallet.sign_sol_tx',
userId,
ip: req.ip || null,
meta: { chain: 'SOL', txLength: transaction.length },
meta: hasTx
? { chain: 'SOL', mode: 'serialized', txLength: transaction.length }
: { chain: 'SOL', mode: 'instructions', count: instructions.length },
});
} catch (auditErr: any) {
logger.error(`Audit DB INSERT MUST succeed: ${auditErr.message}`);
@@ -824,11 +851,20 @@ export const WalletController = {
let result: { signature: string };
try {
result = await signAndBroadcastSolanaTx({
mnemonic,
expectedFromAddress: wallet.address,
serializedTransaction: transaction,
});
if (hasTx) {
result = await signAndBroadcastSolanaTx({
mnemonic,
expectedFromAddress: wallet.address,
serializedTransaction: transaction,
});
} else {
result = await signAndBroadcastSolanaInstructions({
mnemonic,
expectedFromAddress: wallet.address,
instructions,
addressLookupTableAddresses,
});
}
} catch (signErr: any) {
await completeAudit(auditId, 'failure', undefined, 'SOL_SIGN_FAILED');
throw signErr;