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:
@@ -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) {
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user