diff --git a/src/Managing.Web3Proxy/src/generated/gmxsdk/utils/callContract.ts b/src/Managing.Web3Proxy/src/generated/gmxsdk/utils/callContract.ts index 96656aa6..f0b292cc 100644 --- a/src/Managing.Web3Proxy/src/generated/gmxsdk/utils/callContract.ts +++ b/src/Managing.Web3Proxy/src/generated/gmxsdk/utils/callContract.ts @@ -188,6 +188,38 @@ export async function callContract( txnInstance.maxPriorityFeePerGas = gasPriceData.maxPriorityFeePerGas; } + // Check balance before sending transaction + const accountAddress = sdk.config.account as Address; + const balance = await client.getBalance({ address: accountAddress }); + const value = opts.value ? BigInt(opts.value) : 0n; + + // Calculate total cost: gasLimit * gasPrice + value + let totalCost: bigint; + if (gasPriceData.gasPrice !== undefined) { + totalCost = gasLimit * gasPriceData.gasPrice + value; + } else { + // For EIP-1559, use maxFeePerGas for worst-case scenario + totalCost = gasLimit * (gasPriceData.maxFeePerGas || 0n) + value; + } + + if (balance < totalCost) { + const balanceEth = Number(balance) / 1e18; + const requiredEth = Number(totalCost) / 1e18; + const shortfallEth = Number(totalCost - balance) / 1e18; + + const errorMessage = `Insufficient native token balance for gas fees. Address ${accountAddress} has ${balanceEth.toFixed(6)} ETH but needs ${requiredEth.toFixed(6)} ETH (shortfall: ${shortfallEth.toFixed(6)} ETH). Please add more native tokens to your wallet.`; + console.error("❌ Balance check failed:", { + address: accountAddress, + balance: balanceEth.toFixed(6), + required: requiredEth.toFixed(6), + shortfall: shortfallEth.toFixed(6), + gasLimit: gasLimit.toString(), + gasPrice: gasPriceData.gasPrice?.toString() || gasPriceData.maxFeePerGas?.toString(), + value: value.toString() + }); + throw new Error(errorMessage); + } + // Initialize Privy client and send transaction const privy = getPrivyClient(); const networkName = getChainName(sdk.chainId); @@ -218,8 +250,33 @@ export async function callContract( const response = await privy.walletApi.ethereum.sendTransaction(param as any); return response.hash; - } catch (error) { - console.error("Transaction failed:", error); + } catch (error: any) { + console.error("Transaction failed:", error); + + // Check if error is related to insufficient funds + const errorMessage = error?.message || ''; + if (errorMessage.includes('insufficient funds') || errorMessage.includes('insufficient balance')) { + try { + const accountAddress = sdk.config.account as Address; + const balance = await client.getBalance({ address: accountAddress }); + const balanceEth = (Number(balance) / 1e18).toFixed(6); + + throw new Error( + `Insufficient native token balance for transaction. ` + + `Address ${accountAddress} has ${balanceEth} ETH. ` + + `Please add more native tokens (ETH) to your wallet to cover gas fees. ` + + `Original error: ${errorMessage}` + ); + } catch (balanceError) { + // If balance check fails, just throw the original error with a clearer message + throw new Error( + `Insufficient native token balance for transaction. ` + + `Please add more native tokens (ETH) to your wallet to cover gas fees. ` + + `Original error: ${errorMessage}` + ); + } + } + throw error; } } diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 492d9e5c..2de0952e 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -26,7 +26,7 @@ import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../gene import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js'; import {decodeReferralCode, encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js'; import {handleError} from '../../utils/errorHandler.js'; -import {Abi, formatEther, parseEther, zeroHash} from 'viem'; +import {Abi, Address, formatEther, parseEther, zeroHash} from 'viem'; import {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js'; import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js'; import {ContractName, getContract} from '../../generated/gmxsdk/configs/contracts.js'; @@ -1233,14 +1233,83 @@ export const closeGmxPositionImpl = async ( const position = positionsInfo[positionKey]; - console.log(position); + console.log('📊 Position data from wallet:', { + key: positionKey, + sizeInUsd: position.sizeInUsd.toString(), + sizeInTokens: position.sizeInTokens.toString(), + collateralAmount: position.collateralAmount.toString(), + remainingCollateralAmount: position.remainingCollateralAmount.toString(), + marketAddress: position.marketAddress, + collateralTokenAddress: position.collateralTokenAddress, + isLong: position.isLong + }); + + // Re-fetch position data right before closing to ensure we have the latest values + // This ensures parameters match exactly what's on-chain + const latestPositionsInfo = await sdk.positions.getPositionsInfo({ + marketsInfoData, + tokensData, + showPnlInLeverage: true + }); + + const latestPositionKey = Object.keys(latestPositionsInfo).find(key => { + const pos = latestPositionsInfo[key]; + return pos.marketInfo.indexToken.symbol === ticker && pos.isLong === (direction === TradeDirection.Long); + }); + + if (!latestPositionKey) { + throw new Error(`Position no longer exists for ${ticker} ${direction} - it may have been closed already`); + } + + const latestPosition = latestPositionsInfo[latestPositionKey]; + + // Verify all critical parameters match exactly + if (position.sizeInUsd !== latestPosition.sizeInUsd) { + throw new Error( + `Position size mismatch! Wallet has ${latestPosition.sizeInUsd.toString()} but we calculated ${position.sizeInUsd.toString()}. ` + + `Position may have changed. Please retry.` + ); + } + + if (position.sizeInTokens !== latestPosition.sizeInTokens) { + throw new Error( + `Position size in tokens mismatch! Wallet has ${latestPosition.sizeInTokens.toString()} but we calculated ${position.sizeInTokens.toString()}. ` + + `Position may have changed. Please retry.` + ); + } + + if (position.remainingCollateralAmount !== latestPosition.remainingCollateralAmount) { + throw new Error( + `Collateral amount mismatch! Wallet has ${latestPosition.remainingCollateralAmount.toString()} but we calculated ${position.remainingCollateralAmount.toString()}. ` + + `Position may have changed. Please retry.` + ); + } + + if (position.collateralTokenAddress !== latestPosition.collateralTokenAddress) { + throw new Error( + `Collateral token mismatch! Wallet has ${latestPosition.collateralTokenAddress} but we calculated ${position.collateralTokenAddress}. ` + + `Position may have changed. Please retry.` + ); + } + + if (position.marketAddress !== latestPosition.marketAddress) { + throw new Error( + `Market address mismatch! Wallet has ${latestPosition.marketAddress} but we calculated ${position.marketAddress}. ` + + `Position may have changed. Please retry.` + ); + } + + // Use the latest position data to ensure we're closing with exact wallet values + const verifiedPosition = latestPosition; + + console.log('✅ Position parameters verified - all values match wallet state'); const decreaseAmounts: DecreasePositionAmounts = { isFullClose: true, - sizeDeltaUsd: position.sizeInUsd, - sizeDeltaInTokens: position.sizeInTokens, - collateralDeltaUsd: position.remainingCollateralAmount, - collateralDeltaAmount: position.remainingCollateralAmount, + sizeDeltaUsd: verifiedPosition.sizeInUsd, + sizeDeltaInTokens: verifiedPosition.sizeInTokens, + collateralDeltaUsd: verifiedPosition.remainingCollateralAmount, + collateralDeltaAmount: verifiedPosition.remainingCollateralAmount, acceptablePriceDeltaBps: 30n, recommendedAcceptablePriceDeltaBps: 0n, estimatedPnl: 0n, @@ -1267,9 +1336,9 @@ export const closeGmxPositionImpl = async ( decreaseSwapType: DecreasePositionSwapType.NoSwap, indexPrice: 0n, collateralPrice: 0n, - acceptablePrice: position.markPrice, + acceptablePrice: verifiedPosition.markPrice, triggerOrderType: OrderType.MarketDecrease, - triggerPrice: position.markPrice, + triggerPrice: verifiedPosition.markPrice, } const params = { @@ -1279,7 +1348,7 @@ export const closeGmxPositionImpl = async ( isLong: direction === TradeDirection.Long, allowedSlippage: 30, decreaseAmounts, - collateralToken: position.marketInfo.shortToken, + collateralToken: verifiedPosition.marketInfo.shortToken, referralCode: encodeReferralCode("kaigen_ai"), } @@ -2875,6 +2944,90 @@ export const swapGmxTokensImpl = async ( // Calculate the from token amount with proper decimals const fromTokenAmount = BigInt(Math.floor(amount * Math.pow(10, fromTokenData.decimals))); + // Get actual wallet balance for the from token to verify we have enough + let walletBalance: bigint; + if (fromTokenData.isNative) { + // For native tokens (ETH), get balance directly + walletBalance = await sdk.publicClient.getBalance({ address: sdk.account as Address }); + } else { + // For ERC20 tokens, get balance using multicall + const balanceResult = await sdk.executeMulticall({ + token: { + contractAddress: fromTokenData.address, + abiId: "Token", + calls: { + balance: { + methodName: "balanceOf", + params: [sdk.account], + }, + }, + }, + }); + walletBalance = balanceResult.data.token.balance.returnValues[0]; + } + + console.log('💰 Wallet balance check:', { + fromTicker, + fromTokenAddress: fromTokenData.address, + requestedAmount: fromTokenAmount.toString(), + walletBalance: walletBalance.toString(), + hasEnough: walletBalance >= fromTokenAmount + }); + + // Verify the wallet has enough balance + if (walletBalance < fromTokenAmount) { + const balanceFormatted = Number(walletBalance) / Math.pow(10, fromTokenData.decimals); + const requestedFormatted = Number(fromTokenAmount) / Math.pow(10, fromTokenData.decimals); + throw new Error( + `Insufficient ${fromTicker} balance! Wallet has ${balanceFormatted.toFixed(6)} ${fromTicker} ` + + `but trying to swap ${requestedFormatted.toFixed(6)} ${fromTicker}. ` + + `Please ensure you have sufficient balance.` + ); + } + + // Re-fetch balance right before swap to ensure we have the latest value + let latestWalletBalance: bigint; + if (fromTokenData.isNative) { + latestWalletBalance = await sdk.publicClient.getBalance({ address: sdk.account as Address }); + } else { + const latestBalanceResult = await sdk.executeMulticall({ + token: { + contractAddress: fromTokenData.address, + abiId: "Token", + calls: { + balance: { + methodName: "balanceOf", + params: [sdk.account], + }, + }, + }, + }); + latestWalletBalance = latestBalanceResult.data.token.balance.returnValues[0]; + } + + // Verify balance hasn't changed (someone else might have used the wallet) + if (latestWalletBalance !== walletBalance) { + const latestBalanceFormatted = Number(latestWalletBalance) / Math.pow(10, fromTokenData.decimals); + const originalBalanceFormatted = Number(walletBalance) / Math.pow(10, fromTokenData.decimals); + throw new Error( + `Wallet balance changed! Original balance was ${originalBalanceFormatted.toFixed(6)} ${fromTicker} ` + + `but now it's ${latestBalanceFormatted.toFixed(6)} ${fromTicker}. ` + + `Please retry the swap.` + ); + } + + // Verify we still have enough after re-check + if (latestWalletBalance < fromTokenAmount) { + const latestBalanceFormatted = Number(latestWalletBalance) / Math.pow(10, fromTokenData.decimals); + const requestedFormatted = Number(fromTokenAmount) / Math.pow(10, fromTokenData.decimals); + throw new Error( + `Insufficient ${fromTicker} balance after verification! Wallet has ${latestBalanceFormatted.toFixed(6)} ${fromTicker} ` + + `but trying to swap ${requestedFormatted.toFixed(6)} ${fromTicker}. ` + + `Please ensure you have sufficient balance.` + ); + } + + console.log('✅ Token balance verified - wallet has sufficient funds and balance matches'); // Check and handle token allowance for SyntheticsRouter contract await approveTokenForContract(