diff --git a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts index 2e771b78..4b0b7ec2 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/privy.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/privy.ts @@ -11,7 +11,7 @@ import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js' import {TOKENS} from '../../generated/gmxsdk/configs/tokens.js' import {CONTRACTS} from '../../generated/gmxsdk/configs/contracts.js' import {getClientForAddress, getTokenDataFromTicker} from './gmx.js' -import {Address, erc20Abi} from 'viem' +import {Address, erc20Abi, formatEther, parseEther} from 'viem' import {Balance, Chain, Ticker} from '../../generated/ManagingApiTypes.js' import {getCachedPrivySecrets} from './privy-secrets.js' @@ -1296,6 +1296,72 @@ export const sendTokenImpl = async ( const walletId = await getWalletIdFromAddress(senderAddress, fastify); if (ticker === 'ETH') { + // For ETH transfers, we need to reserve gas fees + // Get SDK to access public client for balance and gas estimation + const sdk = await getClientForAddress(senderAddress); + const balance = await sdk.publicClient.getBalance({ address: senderAddress as Address }); + + // Estimate gas for the transfer + // Use a test amount (1 ETH) to estimate gas, then scale if needed + const testAmount = parseEther('1'); + let estimatedGasLimit: bigint; + try { + estimatedGasLimit = await sdk.publicClient.estimateGas({ + to: recipientAddress as Address, + value: testAmount, + account: senderAddress as Address, + }); + } catch (error) { + // If estimation fails, use a conservative default (21,000 is standard for simple transfers) + console.warn('Gas estimation failed, using default:', error); + estimatedGasLimit = 21000n; + } + + // Get gas price + const feeData = await sdk.publicClient.estimateFeesPerGas({ + type: 'legacy', + chain: sdk.chain, + }); + let gasPrice = feeData.gasPrice || 0n; + + // If gas price is 0 or unavailable, use a conservative fallback + // Arbitrum typically has gas prices around 0.1-1 gwei, so 1 gwei is a safe fallback + if (gasPrice === 0n) { + console.warn('Gas price estimation returned 0, using fallback of 1 gwei'); + gasPrice = 1000000000n; // 1 gwei = 1,000,000,000 wei + } + + // Calculate gas cost with 30% buffer for safety + const gasBufferMultiplier = 130n; // 30% buffer + const gasCost = (estimatedGasLimit * gasPrice * gasBufferMultiplier) / 100n; + + // Calculate the maximum amount we can send (balance - gas cost) + const maxSendableAmount = balance > gasCost ? balance - gasCost : 0n; + + // Use the minimum of requested amount and max sendable amount + const actualAmountToSend = amountBigInt > maxSendableAmount ? maxSendableAmount : amountBigInt; + + if (actualAmountToSend <= 0n) { + const balanceEth = formatEther(balance); + const gasCostEth = formatEther(gasCost); + throw new Error( + `Insufficient ETH balance. Balance: ${balanceEth} ETH, ` + + `Estimated gas cost: ${gasCostEth} ETH. ` + + `Cannot send any amount as balance is less than required gas fees.` + ); + } + + // Log if we adjusted the amount + if (actualAmountToSend < amountBigInt) { + const requestedEth = formatEther(amountBigInt); + const sendingEth = formatEther(actualAmountToSend); + const gasCostEth = formatEther(gasCost); + console.log( + `⚠️ Adjusted ETH transfer amount: requested ${requestedEth} ETH, ` + + `sending ${sendingEth} ETH (reserved ${gasCostEth} ETH for gas fees)` + ); + } + // Native ETH transfer: no allowance, no data, value is amount as hex string const { hash } = await privy.wallets().ethereum().sendTransaction( walletId, @@ -1304,7 +1370,7 @@ export const sendTokenImpl = async ( params: { transaction: { to: recipientAddress as Address, - value: '0x' + amountBigInt.toString(16), // value in wei as hex string + value: '0x' + actualAmountToSend.toString(16), // value in wei as hex string chain_id: chainId, } },