Implement balance checks for transactions and swaps in GMX SDK. Enhance error handling to provide detailed feedback on insufficient funds, ensuring users are informed of their wallet status before executing transactions. This includes verifying wallet balances before and after operations to prevent unexpected failures.

This commit is contained in:
2025-12-29 20:34:42 +07:00
parent 10691ab0b8
commit bdf62f6c9e
2 changed files with 221 additions and 11 deletions

View File

@@ -188,6 +188,38 @@ export async function callContract(
txnInstance.maxPriorityFeePerGas = gasPriceData.maxPriorityFeePerGas; 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 // Initialize Privy client and send transaction
const privy = getPrivyClient(); const privy = getPrivyClient();
const networkName = getChainName(sdk.chainId); const networkName = getChainName(sdk.chainId);
@@ -218,8 +250,33 @@ export async function callContract(
const response = await privy.walletApi.ethereum.sendTransaction(param as any); const response = await privy.walletApi.ethereum.sendTransaction(param as any);
return response.hash; return response.hash;
} catch (error) { } catch (error: any) {
console.error("Transaction failed:", error); 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; throw error;
} }
} }

View File

@@ -26,7 +26,7 @@ import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../gene
import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js'; import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js';
import {decodeReferralCode, encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js'; import {decodeReferralCode, encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js';
import {handleError} from '../../utils/errorHandler.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 {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js';
import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js'; import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js';
import {ContractName, getContract} from '../../generated/gmxsdk/configs/contracts.js'; import {ContractName, getContract} from '../../generated/gmxsdk/configs/contracts.js';
@@ -1233,14 +1233,83 @@ export const closeGmxPositionImpl = async (
const position = positionsInfo[positionKey]; 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 = { const decreaseAmounts: DecreasePositionAmounts = {
isFullClose: true, isFullClose: true,
sizeDeltaUsd: position.sizeInUsd, sizeDeltaUsd: verifiedPosition.sizeInUsd,
sizeDeltaInTokens: position.sizeInTokens, sizeDeltaInTokens: verifiedPosition.sizeInTokens,
collateralDeltaUsd: position.remainingCollateralAmount, collateralDeltaUsd: verifiedPosition.remainingCollateralAmount,
collateralDeltaAmount: position.remainingCollateralAmount, collateralDeltaAmount: verifiedPosition.remainingCollateralAmount,
acceptablePriceDeltaBps: 30n, acceptablePriceDeltaBps: 30n,
recommendedAcceptablePriceDeltaBps: 0n, recommendedAcceptablePriceDeltaBps: 0n,
estimatedPnl: 0n, estimatedPnl: 0n,
@@ -1267,9 +1336,9 @@ export const closeGmxPositionImpl = async (
decreaseSwapType: DecreasePositionSwapType.NoSwap, decreaseSwapType: DecreasePositionSwapType.NoSwap,
indexPrice: 0n, indexPrice: 0n,
collateralPrice: 0n, collateralPrice: 0n,
acceptablePrice: position.markPrice, acceptablePrice: verifiedPosition.markPrice,
triggerOrderType: OrderType.MarketDecrease, triggerOrderType: OrderType.MarketDecrease,
triggerPrice: position.markPrice, triggerPrice: verifiedPosition.markPrice,
} }
const params = { const params = {
@@ -1279,7 +1348,7 @@ export const closeGmxPositionImpl = async (
isLong: direction === TradeDirection.Long, isLong: direction === TradeDirection.Long,
allowedSlippage: 30, allowedSlippage: 30,
decreaseAmounts, decreaseAmounts,
collateralToken: position.marketInfo.shortToken, collateralToken: verifiedPosition.marketInfo.shortToken,
referralCode: encodeReferralCode("kaigen_ai"), referralCode: encodeReferralCode("kaigen_ai"),
} }
@@ -2875,6 +2944,90 @@ export const swapGmxTokensImpl = async (
// Calculate the from token amount with proper decimals // Calculate the from token amount with proper decimals
const fromTokenAmount = BigInt(Math.floor(amount * Math.pow(10, fromTokenData.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 // Check and handle token allowance for SyntheticsRouter contract
await approveTokenForContract( await approveTokenForContract(