From 6f01f4af34bae32effb3a126add655e866a0bbd0 Mon Sep 17 00:00:00 2001 From: ZOMBIIIIIII <120676065+Metaton241@users.noreply.github.com> Date: Wed, 17 Jun 2026 23:23:41 +0300 Subject: [PATCH] initnmjnjnj --- apps/api/src/services/uniswap-lp.service.ts | 32 +++++----- apps/api/src/services/wallet-signer-bridge.ts | 62 ++++++++++++------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/apps/api/src/services/uniswap-lp.service.ts b/apps/api/src/services/uniswap-lp.service.ts index fad6ffa..c0a3d78 100644 --- a/apps/api/src/services/uniswap-lp.service.ts +++ b/apps/api/src/services/uniswap-lp.service.ts @@ -463,30 +463,32 @@ export async function executeAddLiquidity(p: { throw badRequest(`Недостаточно ${pool.sym1}: нужно ${formatSmallestUnits(a1.toString(), pool.dec1)}, есть ${formatSmallestUnits(bal.toString(), pool.dec1)}`); } } + // WETH-нога при useNativeEth: заворачиваем ТОЛЬКО недостающее (учитываем уже имеющийся WETH от прошлых + // попыток — не плодим WETH и не требуем лишний ETH). wethDeficit переиспользуется в шаге wrap ниже. + let wethDeficit = 0n; if (p.useNativeEth) { const wrapAmt = t0IsWeth ? a0 : t1IsWeth ? a1 : 0n; - if (wrapAmt > 0n) { + const wethLegAddr = t0IsWeth ? pool.token0 : t1IsWeth ? pool.token1 : ''; + if (wrapAmt > 0n && wethLegAddr) { + let wethBal = 0n; + try { wethBal = await readErc20Balance('ETH', wethLegAddr, p.expectedFromAddress); } catch { /* assume 0 */ } + wethDeficit = wethBal >= wrapAmt ? 0n : wrapAmt - wethBal; const ethBal = await readEvmNativeBalance('ETH', p.expectedFromAddress); - const need = wrapAmt + ethers.utils.parseEther('0.001').toBigInt(); // wrap + буфер на газ + const need = wethDeficit + ethers.utils.parseEther('0.001').toBigInt(); // недостающий WETH + буфер на газ if (ethBal < need) { - throw badRequest(`Недостаточно ETH: нужно ~${formatSmallestUnits(need.toString(), 18)} (wrap + газ), есть ${formatSmallestUnits(ethBal.toString(), 18)}`); + throw badRequest(`Недостаточно ETH: нужно ~${formatSmallestUnits(need.toString(), 18)} (wrap недостающего WETH + газ), есть ${formatSmallestUnits(ethBal.toString(), 18)}`); } } } - // useNativeEth: если у пула есть WETH-нога — заворачиваем нужный ETH в WETH (WETH.deposit, ждёт - // 1 conf). Дальше WETH = обычный ERC20: fee/approve/mint без изменений. Юзеру нужен ETH + второй токен. + // useNativeEth: заворачиваем ТОЛЬКО недостающий WETH (wethDeficit). Если WETH уже хватает — пропускаем + // (используем имеющийся). Дальше WETH = обычный ERC20: fee/approve/mint без изменений. let wrapTxid: string | undefined; - if (p.useNativeEth) { - let wrapAmount = 0n; - if (isWeth(pool.token0)) wrapAmount = a0; - else if (isWeth(pool.token1)) wrapAmount = a1; - if (wrapAmount > 0n) { - const w = await signAndBroadcastWrapEth({ - mnemonic: p.mnemonic, expectedFromAddress: p.expectedFromAddress, amountWei: wrapAmount.toString(), - }); - wrapTxid = w.txid; - } + if (p.useNativeEth && wethDeficit > 0n) { + const w = await signAndBroadcastWrapEth({ + mnemonic: p.mnemonic, expectedFromAddress: p.expectedFromAddress, amountWei: wethDeficit.toString(), + }); + wrapTxid = w.txid; } // app fee 0.7% по каждому токену (net = то что реально пойдёт в пул) diff --git a/apps/api/src/services/wallet-signer-bridge.ts b/apps/api/src/services/wallet-signer-bridge.ts index 744c117..9d85b58 100644 --- a/apps/api/src/services/wallet-signer-bridge.ts +++ b/apps/api/src/services/wallet-signer-bridge.ts @@ -101,7 +101,7 @@ export async function signAndBroadcastEvmApprove(p: SignEvmApproveParams): Promi const signer = wallet.connect(provider); const token = new ethers.Contract(p.token, ERC20_ABI, signer); - // Fee tier: используем provider.getFeeData() — это OK для approve (low priority). + // Fee tier: provider.getFeeData() — OK для approve (low priority). Считаем один раз на все tx. const feeData = await provider.getFeeData(); const capWei = ethers.utils.parseUnits(String(MAX_GAS_PRICE_GWEI), 'gwei'); let maxFeePerGas = feeData.maxFeePerGas ?? ethers.utils.parseUnits('30', 'gwei'); @@ -109,27 +109,47 @@ export async function signAndBroadcastEvmApprove(p: SignEvmApproveParams): Promi if (maxFeePerGas.gt(capWei)) maxFeePerGas = capWei; if (maxPriorityFeePerGas.gt(maxFeePerGas)) maxPriorityFeePerGas = maxFeePerGas; - const nonce = await provider.getTransactionCount(wallet.address, 'pending'); - const data = token.interface.encodeFunctionData('approve', [p.spender, ethers.BigNumber.from(p.amount)]); + // Отправить один approve(spender, value), дождаться 1 conf. Nonce берётся 'pending' перед КАЖДОЙ tx + // (после reset предыдущая уже подтверждена → следующая получит инкремент). + const sendOne = async (value: ethers.BigNumber): Promise => { + const nonce = await provider.getTransactionCount(wallet.address, 'pending'); + const data = token.interface.encodeFunctionData('approve', [p.spender, value]); + const sent = await signer.sendTransaction({ + to: p.token, data, value: 0, chainId: expectedChainId, nonce, + gasLimit: APPROVE_GAS_LIMIT, type: 2, maxFeePerGas, maxPriorityFeePerGas, + }); + await Promise.race([ + sent.wait(1), + new Promise((_, reject) => setTimeout(() => reject(new Error('approve confirm timeout')), 60_000)), + ]); + return sent.hash; + }; - const sent = await signer.sendTransaction({ - to: p.token, - data, - value: 0, - chainId: expectedChainId, - nonce, - gasLimit: APPROVE_GAS_LIMIT, - type: 2, - maxFeePerGas, - maxPriorityFeePerGas, - }); - // Wait 1 conf чтобы bridge tx (next) видел updated allowance - await Promise.race([ - sent.wait(1), - new Promise((_, reject) => setTimeout(() => reject(new Error('approve confirm timeout')), 60_000)), - ]); - logger.info(`EVM approve confirmed: chain=${p.chain} token=${p.token} spender=${p.spender} amount=${p.amount} txid=${sent.hash}`); - return { txid: sent.hash }; + const needed = ethers.BigNumber.from(p.amount); + + // Текущий allowance — для idempotency + USDT-safe reset. (amount '0'/revoke ниже не попадает в эти ветки.) + let current = ethers.BigNumber.from(0); + try { + current = await token.allowance(wallet.address, p.spender); + } catch (err: any) { + logger.warn(`EVM approve: allowance read failed (assume 0): ${err?.message}`); + } + + // Уже достаточно → не шлём лишнюю tx (повторы, и WETH-нога с остаточным allowance). + if (needed.gt(0) && current.gte(needed)) { + logger.info(`EVM approve skip: allowance ${current.toString()} >= needed ${needed.toString()} (token=${p.token} spender=${p.spender})`); + return { txid: '' }; + } + + // USDT-style: контракт реверит approve(ненулевое) пока allowance != 0 → сначала reset в 0, потом ставим нужное. + if (needed.gt(0) && current.gt(0)) { + logger.info(`EVM approve reset-to-0 (USDT-safe): ${current.toString()} → 0 → ${needed.toString()} (token=${p.token} spender=${p.spender})`); + await sendOne(ethers.constants.Zero); + } + + const txid = await sendOne(needed); + logger.info(`EVM approve confirmed: chain=${p.chain} token=${p.token} spender=${p.spender} amount=${p.amount} txid=${txid}`); + return { txid }; } /**