Add claim and swap
This commit is contained in:
@@ -19,14 +19,25 @@ import {ARBITRUM} from '../../generated/gmxsdk/configs/chains.js'
|
|||||||
import {TokenData, TokensData} from '../../generated/gmxsdk/types/tokens.js';
|
import {TokenData, TokensData} from '../../generated/gmxsdk/types/tokens.js';
|
||||||
import {getByKey} from '../../generated/gmxsdk/utils/objects.js';
|
import {getByKey} from '../../generated/gmxsdk/utils/objects.js';
|
||||||
import {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js';
|
import {GmxSdkConfig} from '../../generated/gmxsdk/types/sdk.js';
|
||||||
import {PositionIncreaseParams} from '../../generated/gmxsdk/modules/orders/helpers.js';
|
import {PositionIncreaseParams, SwapParams} from '../../generated/gmxsdk/modules/orders/helpers.js';
|
||||||
import {bigintToNumber, numberToBigint, PRECISION_DECIMALS} from '../../generated/gmxsdk/utils/numbers.js';
|
import {
|
||||||
|
basisPointsToFloat,
|
||||||
|
bigintToNumber,
|
||||||
|
numberToBigint,
|
||||||
|
PRECISION_DECIMALS
|
||||||
|
} from '../../generated/gmxsdk/utils/numbers.js';
|
||||||
import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js';
|
import {DecreasePositionSwapType, OrderType, PositionOrderInfo} from '../../generated/gmxsdk/types/orders.js';
|
||||||
import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js';
|
import {DecreasePositionAmounts} from '../../generated/gmxsdk/types/trade.js';
|
||||||
import {encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js';
|
import {decodeReferralCode, encodeReferralCode} from '../../generated/gmxsdk/utils/referrals.js';
|
||||||
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
|
import {formatUsd} from '../../generated/gmxsdk/utils/numbers/formatting.js';
|
||||||
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
|
import {calculateDisplayDecimals} from '../../generated/gmxsdk/utils/numbers/index.js';
|
||||||
import {handleError} from '../../utils/errorHandler.js';
|
import {handleError} from '../../utils/errorHandler.js';
|
||||||
|
import {Abi, zeroHash} from 'viem';
|
||||||
|
import {CLAIMABLE_FUNDING_AMOUNT} from '../../generated/gmxsdk/configs/dataStore.js';
|
||||||
|
import {hashDataMap, hashString} from '../../generated/gmxsdk/utils/hash.js';
|
||||||
|
import {getContract} from '../../generated/gmxsdk/configs/contracts.js';
|
||||||
|
import {abis} from '../../generated/gmxsdk/abis/index.js';
|
||||||
|
import {approveContractImpl, getTokenAllowance} from './privy.js';
|
||||||
|
|
||||||
// Cache implementation for markets info data
|
// Cache implementation for markets info data
|
||||||
interface CacheEntry {
|
interface CacheEntry {
|
||||||
@@ -122,6 +133,47 @@ const cancelOrdersSchema = z.object({
|
|||||||
ticker: z.string().nonempty()
|
ticker: z.string().nonempty()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Schema for claim funding fees request
|
||||||
|
const claimFundingFeesSchema = z.object({
|
||||||
|
account: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for claim price impact request
|
||||||
|
const claimPriceImpactSchema = z.object({
|
||||||
|
account: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for get price impact rebates request
|
||||||
|
const getPriceImpactRebatesSchema = z.object({
|
||||||
|
account: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for claim UI fees request
|
||||||
|
const claimUiFeesSchema = z.object({
|
||||||
|
account: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for get claimable UI fees request
|
||||||
|
const getClaimableUiFeesSchema = z.object({
|
||||||
|
account: z.string().nonempty()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for swap-tokens request
|
||||||
|
const swapTokensSchema = z.object({
|
||||||
|
account: z.string().nonempty(),
|
||||||
|
fromTicker: z.string().nonempty(),
|
||||||
|
toTicker: z.string().nonempty(),
|
||||||
|
amount: z.number().positive(),
|
||||||
|
orderType: z.enum(['market', 'limit']).default('market'),
|
||||||
|
triggerRatio: z.number().optional(),
|
||||||
|
allowedSlippage: z.number().min(0).max(100).default(0.5)
|
||||||
|
}).refine((data) => data.fromTicker !== data.toTicker, {
|
||||||
|
message: "From and to tickers must be different",
|
||||||
|
path: ["toTicker"]
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a GMX SDK client initialized for the given address
|
* Gets a GMX SDK client initialized for the given address
|
||||||
* If a walletId is provided, it will be used with Privy for signing
|
* If a walletId is provided, it will be used with Privy for signing
|
||||||
@@ -860,6 +912,13 @@ export default fp(async (fastify) => {
|
|||||||
fastify.decorateRequest('closeGmxPosition', closeGmxPosition)
|
fastify.decorateRequest('closeGmxPosition', closeGmxPosition)
|
||||||
fastify.decorateRequest('getGmxTrade', getGmxTrade)
|
fastify.decorateRequest('getGmxTrade', getGmxTrade)
|
||||||
fastify.decorateRequest('getGmxPositions', getGmxPositions)
|
fastify.decorateRequest('getGmxPositions', getGmxPositions)
|
||||||
|
fastify.decorateRequest('getGmxRebateStats', getGmxRebateStats)
|
||||||
|
fastify.decorateRequest('getClaimableFundingFees', getClaimableFundingFees)
|
||||||
|
fastify.decorateRequest('claimGmxFundingFees', claimGmxFundingFees)
|
||||||
|
fastify.decorateRequest('getClaimableUiFees', getClaimableUiFees)
|
||||||
|
fastify.decorateRequest('claimGmxUiFees', claimGmxUiFees)
|
||||||
|
fastify.decorateRequest('swapGmxTokens', swapGmxTokens)
|
||||||
|
fastify.decorateRequest('checkGmxTokenAllowances', checkGmxTokenAllowances)
|
||||||
|
|
||||||
// Pre-populate and refresh the markets cache on startup
|
// Pre-populate and refresh the markets cache on startup
|
||||||
fastify.addHook('onReady', async () => {
|
fastify.addHook('onReady', async () => {
|
||||||
@@ -873,3 +932,846 @@ export default fp(async (fastify) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getGmxRebateStatsImpl = async (
|
||||||
|
sdk: GmxSdk
|
||||||
|
): Promise<{
|
||||||
|
totalRebateUsd: number;
|
||||||
|
discountUsd: number;
|
||||||
|
volume: number;
|
||||||
|
tier: number;
|
||||||
|
rebateFactor: number;
|
||||||
|
discountFactor: number;
|
||||||
|
} | null> => {
|
||||||
|
try {
|
||||||
|
// Get the referral storage contract address
|
||||||
|
|
||||||
|
const referralStorageAddress = getContract(sdk.chainId, "ReferralStorage");
|
||||||
|
|
||||||
|
// Get user referral code
|
||||||
|
const userRefCodeRes = await sdk.executeMulticall({
|
||||||
|
referralStorage: {
|
||||||
|
contractAddress: referralStorageAddress,
|
||||||
|
abiId: "ReferralStorage",
|
||||||
|
calls: {
|
||||||
|
traderReferralCodes: {
|
||||||
|
methodName: "traderReferralCodes",
|
||||||
|
params: [sdk.account],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("traderReferralCodes", userRefCodeRes.data.referralStorage.traderReferralCodes.returnValues)
|
||||||
|
|
||||||
|
const userReferralCode = userRefCodeRes.data.referralStorage.traderReferralCodes.returnValues[0];
|
||||||
|
const userReferralCodeString = decodeReferralCode(userReferralCode);
|
||||||
|
|
||||||
|
console.log("userReferralCodeAfterDecode", userReferralCodeString)
|
||||||
|
|
||||||
|
// If no referral code, return default values
|
||||||
|
if (!userReferralCode || userReferralCode === zeroHash) {
|
||||||
|
return {
|
||||||
|
totalRebateUsd: 0,
|
||||||
|
discountUsd: 0,
|
||||||
|
volume: 0,
|
||||||
|
tier: 0,
|
||||||
|
rebateFactor: 0,
|
||||||
|
discountFactor: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get code owner and affiliate tier
|
||||||
|
const [codeOwnerRes, affiliateTierRes] = await Promise.all([
|
||||||
|
sdk.executeMulticall({
|
||||||
|
referralStorage: {
|
||||||
|
contractAddress: referralStorageAddress,
|
||||||
|
abiId: "ReferralStorage",
|
||||||
|
calls: {
|
||||||
|
codeOwner: {
|
||||||
|
methodName: "codeOwners",
|
||||||
|
params: [userReferralCodeString],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
sdk.executeMulticall({
|
||||||
|
referralStorage: {
|
||||||
|
contractAddress: referralStorageAddress,
|
||||||
|
abiId: "ReferralStorage",
|
||||||
|
calls: {
|
||||||
|
referrerTiers: {
|
||||||
|
methodName: "referrerTiers",
|
||||||
|
params: [sdk.account],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
const codeOwner = codeOwnerRes.data.referralStorage.codeOwner.returnValues[0];
|
||||||
|
const tierId = affiliateTierRes.data.referralStorage.referrerTiers.returnValues[0];
|
||||||
|
|
||||||
|
// Get tier information
|
||||||
|
const tiersRes = await sdk.executeMulticall({
|
||||||
|
referralStorage: {
|
||||||
|
contractAddress: referralStorageAddress,
|
||||||
|
abiId: "ReferralStorage",
|
||||||
|
calls: {
|
||||||
|
tiers: {
|
||||||
|
methodName: "tiers",
|
||||||
|
params: [tierId],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [totalRebate, discountShare] = tiersRes.data.referralStorage.tiers.returnValues ?? [0n, 0n];
|
||||||
|
|
||||||
|
// Get custom discount share if available
|
||||||
|
let customDiscountShare = 0n;
|
||||||
|
if (codeOwner) {
|
||||||
|
const customDiscountRes = await sdk.executeMulticall({
|
||||||
|
referralStorage: {
|
||||||
|
contractAddress: referralStorageAddress,
|
||||||
|
abiId: "ReferralStorage",
|
||||||
|
calls: {
|
||||||
|
referrerDiscountShares: {
|
||||||
|
methodName: "referrerDiscountShares",
|
||||||
|
params: [codeOwner],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
customDiscountShare = customDiscountRes.data.referralStorage.referrerDiscountShares.returnValues[0] || 0n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalDiscountShare = customDiscountShare > 0n ? customDiscountShare : discountShare;
|
||||||
|
|
||||||
|
// Convert bigint values to numbers for JSON serialization
|
||||||
|
const totalRebateFactor = basisPointsToFloat(totalRebate);
|
||||||
|
const discountFactor = basisPointsToFloat(finalDiscountShare);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalRebateUsd: Number(totalRebate) / 1e4, // Convert from basis points to decimal
|
||||||
|
discountUsd: Number(finalDiscountShare) / 1e4, // Convert from basis points to decimal
|
||||||
|
volume: 0, // Volume data would need to be fetched from different endpoint
|
||||||
|
tier: Number(tierId),
|
||||||
|
rebateFactor: Number(totalRebateFactor),
|
||||||
|
discountFactor: Number(discountFactor)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting GMX rebate stats:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getGmxRebateStats(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
account: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const sdk = await this.getClientForAddress(account);
|
||||||
|
const rebateStats = await getGmxRebateStatsImpl(sdk);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
rebateStats
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(this, reply, error, 'getGmxRebateStats');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for claimable funding data per market
|
||||||
|
*/
|
||||||
|
interface ClaimableFundingData {
|
||||||
|
[marketAddress: string]: {
|
||||||
|
claimableFundingAmountLong: number;
|
||||||
|
claimableFundingAmountShort: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for funding fees claim parameters
|
||||||
|
*/
|
||||||
|
interface FundingFeesClaimData {
|
||||||
|
marketAddresses: string[];
|
||||||
|
tokenAddresses: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation function to get claimable funding fees
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @returns Claimable funding data
|
||||||
|
*/
|
||||||
|
export const getClaimableFundingFeesImpl = async (
|
||||||
|
sdk: GmxSdk
|
||||||
|
): Promise<ClaimableFundingData> => {
|
||||||
|
try {
|
||||||
|
const { marketsInfoData } = await getMarketsInfoWithCache(sdk);
|
||||||
|
|
||||||
|
if (!marketsInfoData) {
|
||||||
|
throw new Error("No markets info data available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketAddresses = Object.keys(marketsInfoData);
|
||||||
|
|
||||||
|
if (marketAddresses.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build multicall request for all markets
|
||||||
|
const multicallRequest = marketAddresses.reduce((request, marketAddress) => {
|
||||||
|
const market = marketsInfoData[marketAddress];
|
||||||
|
|
||||||
|
if (!market) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = hashDataMap({
|
||||||
|
claimableFundingAmountLong: [
|
||||||
|
["bytes32", "address", "address", "address"],
|
||||||
|
[CLAIMABLE_FUNDING_AMOUNT, marketAddress, market.longToken.address, sdk.account],
|
||||||
|
],
|
||||||
|
claimableFundingAmountShort: [
|
||||||
|
["bytes32", "address", "address", "address"],
|
||||||
|
[CLAIMABLE_FUNDING_AMOUNT, marketAddress, market.shortToken.address, sdk.account],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
request[marketAddress] = {
|
||||||
|
contractAddress: getContract(sdk.chainId, "DataStore"),
|
||||||
|
abiId: "DataStore",
|
||||||
|
calls: {
|
||||||
|
claimableFundingAmountLong: {
|
||||||
|
methodName: "getUint",
|
||||||
|
params: [keys.claimableFundingAmountLong],
|
||||||
|
},
|
||||||
|
claimableFundingAmountShort: {
|
||||||
|
methodName: "getUint",
|
||||||
|
params: [keys.claimableFundingAmountShort],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const result = await sdk.executeMulticall(multicallRequest);
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
return Object.entries(result.data).reduce((claimableFundingData, [marketAddress, callsResult]: [string, any]) => {
|
||||||
|
const market = marketsInfoData[marketAddress];
|
||||||
|
|
||||||
|
if (!market) {
|
||||||
|
return claimableFundingData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get market divisor for proper decimal conversion
|
||||||
|
const marketDivisor = 1; // You might need to implement getMarketDivisor function
|
||||||
|
|
||||||
|
claimableFundingData[marketAddress] = {
|
||||||
|
claimableFundingAmountLong: Number(callsResult.claimableFundingAmountLong.returnValues[0]) / marketDivisor,
|
||||||
|
claimableFundingAmountShort: Number(callsResult.claimableFundingAmountShort.returnValues[0]) / marketDivisor,
|
||||||
|
};
|
||||||
|
|
||||||
|
return claimableFundingData;
|
||||||
|
}, {} as ClaimableFundingData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting claimable funding fees:', error);
|
||||||
|
throw new Error(`Failed to get claimable funding fees: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation function to claim funding fees
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @returns Transaction hash
|
||||||
|
*/
|
||||||
|
export const claimGmxFundingFeesImpl = async (
|
||||||
|
sdk: GmxSdk
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
// First get claimable funding data to determine what to claim
|
||||||
|
const claimableFundingData = await getClaimableFundingFeesImpl(sdk);
|
||||||
|
|
||||||
|
const marketAddresses: string[] = [];
|
||||||
|
const tokenAddresses: string[] = [];
|
||||||
|
|
||||||
|
// Build arrays of markets and tokens that have claimable amounts
|
||||||
|
Object.entries(claimableFundingData).forEach(([marketAddress, data]) => {
|
||||||
|
if (data.claimableFundingAmountLong > 0) {
|
||||||
|
marketAddresses.push(marketAddress);
|
||||||
|
// Get the market info to find the long token address
|
||||||
|
const { marketsInfoData } = marketsCache.get(`markets_${sdk.chainId}`)?.data || {};
|
||||||
|
if (marketsInfoData?.[marketAddress]) {
|
||||||
|
tokenAddresses.push(marketsInfoData[marketAddress].longToken.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.claimableFundingAmountShort > 0) {
|
||||||
|
marketAddresses.push(marketAddress);
|
||||||
|
// Get the market info to find the short token address
|
||||||
|
const { marketsInfoData } = marketsCache.get(`markets_${sdk.chainId}`)?.data || {};
|
||||||
|
if (marketsInfoData?.[marketAddress]) {
|
||||||
|
tokenAddresses.push(marketsInfoData[marketAddress].shortToken.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (marketAddresses.length === 0) {
|
||||||
|
throw new Error("No funding fees available to claim");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ExchangeRouter contract address
|
||||||
|
const exchangeRouterAddress = getContract(sdk.chainId, "ExchangeRouter");
|
||||||
|
|
||||||
|
console.log("marketAddresses", marketAddresses)
|
||||||
|
console.log("tokenAddresses", tokenAddresses)
|
||||||
|
console.log("account", sdk.account)
|
||||||
|
|
||||||
|
// Execute the claim funding fees transaction using sdk.callContract
|
||||||
|
await sdk.callContract(
|
||||||
|
exchangeRouterAddress,
|
||||||
|
abis.ExchangeRouter as Abi,
|
||||||
|
"claimFundingFees",
|
||||||
|
[marketAddresses, tokenAddresses, sdk.account]
|
||||||
|
);
|
||||||
|
|
||||||
|
return "funding_fees_claimed"; // Return a success indicator
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error claiming funding fees:', error);
|
||||||
|
throw new Error(`Failed to claim funding fees: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets claimable funding fees on GMX
|
||||||
|
* @param this The FastifyRequest instance
|
||||||
|
* @param reply The FastifyReply instance
|
||||||
|
* @param account The wallet address of the user
|
||||||
|
* @returns The response object with success status and claimable funding data
|
||||||
|
*/
|
||||||
|
export async function getClaimableFundingFees(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
account: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Validate the request parameters
|
||||||
|
claimFundingFeesSchema.parse({ account });
|
||||||
|
|
||||||
|
// Get client for the address
|
||||||
|
const sdk = await this.getClientForAddress(account);
|
||||||
|
|
||||||
|
// Call the implementation function
|
||||||
|
const claimableFundingData = await getClaimableFundingFeesImpl(sdk);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
claimableFundingData
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(this, reply, error, 'gmx/get-claimable-funding-fees');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims funding fees on GMX
|
||||||
|
* @param this The FastifyRequest instance
|
||||||
|
* @param reply The FastifyReply instance
|
||||||
|
* @param account The wallet address of the user
|
||||||
|
* @returns The response object with success status and transaction hash
|
||||||
|
*/
|
||||||
|
export async function claimGmxFundingFees(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
account: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Validate the request parameters
|
||||||
|
claimFundingFeesSchema.parse({ account });
|
||||||
|
|
||||||
|
// Get client for the address
|
||||||
|
const sdk = await this.getClientForAddress(account);
|
||||||
|
|
||||||
|
// Call the implementation function
|
||||||
|
const hash = await claimGmxFundingFeesImpl(sdk);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
hash
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(this, reply, error, 'gmx/claim-funding-fees');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for raw claimable collateral from subgraph
|
||||||
|
*/
|
||||||
|
interface RawClaimableCollateral {
|
||||||
|
id: string;
|
||||||
|
marketAddress: string;
|
||||||
|
tokenAddress: string;
|
||||||
|
timeKey: number;
|
||||||
|
value: string;
|
||||||
|
factor: string;
|
||||||
|
factorByTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for processed rebate info item
|
||||||
|
*/
|
||||||
|
interface RebateInfoItem {
|
||||||
|
factor: bigint;
|
||||||
|
value: bigint;
|
||||||
|
valueByFactor: bigint;
|
||||||
|
timeKey: number;
|
||||||
|
marketAddress: string;
|
||||||
|
tokenAddress: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for rebates info result
|
||||||
|
*/
|
||||||
|
interface RebatesInfoResult {
|
||||||
|
accruedPositionPriceImpactFees: RebateInfoItem[];
|
||||||
|
claimablePositionPriceImpactFees: RebateInfoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for claimable UI fee data per market
|
||||||
|
*/
|
||||||
|
interface ClaimableUiFeeData {
|
||||||
|
[marketAddress: string]: {
|
||||||
|
claimableUiFeeAmount: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation function to get claimable UI fees
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @returns Claimable UI fee data
|
||||||
|
*/
|
||||||
|
export const getClaimableUiFeesImpl = async (
|
||||||
|
sdk: GmxSdk
|
||||||
|
): Promise<ClaimableUiFeeData> => {
|
||||||
|
try {
|
||||||
|
const { marketsInfoData } = await getMarketsInfoWithCache(sdk);
|
||||||
|
|
||||||
|
if (!marketsInfoData) {
|
||||||
|
throw new Error("No markets info data available");
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketAddresses = Object.keys(marketsInfoData);
|
||||||
|
|
||||||
|
if (marketAddresses.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get UI fee receiver from SDK config
|
||||||
|
const uiFeeReceiver = "0xF9f04a745Db54B25bB8B345a1da74D4E3c38c8aB";
|
||||||
|
|
||||||
|
// Build multicall request for all markets
|
||||||
|
const multicallRequest = marketAddresses.reduce((request, marketAddress) => {
|
||||||
|
const market = marketsInfoData[marketAddress];
|
||||||
|
|
||||||
|
if (!market) {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLAIMABLE_UI_FEE_AMOUNT = hashString("CLAIMABLE_UI_FEE_AMOUNT");
|
||||||
|
|
||||||
|
const keys = hashDataMap({
|
||||||
|
claimableUiFeeAmount: [
|
||||||
|
["bytes32", "address", "address", "address"],
|
||||||
|
[CLAIMABLE_UI_FEE_AMOUNT, marketAddress, market.longToken.address, uiFeeReceiver],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
request[marketAddress] = {
|
||||||
|
contractAddress: getContract(sdk.chainId, "DataStore"),
|
||||||
|
abiId: "DataStore",
|
||||||
|
calls: {
|
||||||
|
claimableUiFeeAmount: {
|
||||||
|
methodName: "getUint",
|
||||||
|
params: [keys.claimableUiFeeAmount],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const result = await sdk.executeMulticall(multicallRequest);
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
return Object.entries(result.data).reduce((claimableUiFeeData, [marketAddress, callsResult]: [string, any]) => {
|
||||||
|
const market = marketsInfoData[marketAddress];
|
||||||
|
|
||||||
|
if (!market) {
|
||||||
|
return claimableUiFeeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert from wei to token units (assuming 6 decimals for USDC-like tokens)
|
||||||
|
const tokenDecimals = market.longToken.decimals || 6;
|
||||||
|
|
||||||
|
claimableUiFeeData[marketAddress] = {
|
||||||
|
claimableUiFeeAmount: Number(callsResult.claimableUiFeeAmount.returnValues[0]) / Math.pow(10, tokenDecimals),
|
||||||
|
};
|
||||||
|
|
||||||
|
return claimableUiFeeData;
|
||||||
|
}, {} as ClaimableUiFeeData);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting claimable UI fees:', error);
|
||||||
|
throw new Error(`Failed to get claimable UI fees: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets claimable UI fees on GMX
|
||||||
|
* @param this The FastifyRequest instance
|
||||||
|
* @param reply The FastifyReply instance
|
||||||
|
* @param account The wallet address of the user
|
||||||
|
* @returns The response object with success status and claimable UI fee data
|
||||||
|
*/
|
||||||
|
export async function getClaimableUiFees(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
account: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Validate the request parameters
|
||||||
|
getClaimableUiFeesSchema.parse({ account });
|
||||||
|
|
||||||
|
// Get client for the address
|
||||||
|
const sdk = await this.getClientForAddress(account);
|
||||||
|
|
||||||
|
// Call the implementation function
|
||||||
|
const claimableUiFeeData = await getClaimableUiFeesImpl(sdk);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
claimableUiFeeData
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(this, reply, error, 'gmx/get-claimable-ui-fees');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation function to claim UI fees
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @returns Transaction hash
|
||||||
|
*/
|
||||||
|
export const claimGmxUiFeesImpl = async (
|
||||||
|
sdk: GmxSdk
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
// First get claimable UI fee data to determine what to claim
|
||||||
|
const claimableUiFeeData = await getClaimableUiFeesImpl(sdk);
|
||||||
|
|
||||||
|
const marketAddresses: string[] = [];
|
||||||
|
const tokenAddresses: string[] = [];
|
||||||
|
|
||||||
|
// Build arrays of markets and tokens that have claimable amounts
|
||||||
|
Object.entries(claimableUiFeeData).forEach(([marketAddress, data]) => {
|
||||||
|
const { marketsInfoData } = marketsCache.get(`markets_${sdk.chainId}`)?.data || {};
|
||||||
|
|
||||||
|
if (!marketsInfoData?.[marketAddress]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const marketInfo = marketsInfoData[marketAddress];
|
||||||
|
|
||||||
|
marketAddresses.push(marketAddress);
|
||||||
|
tokenAddresses.push(marketInfo.longToken.address);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Get the ExchangeRouter contract address
|
||||||
|
const exchangeRouterAddress = getContract(sdk.chainId, "ExchangeRouter");
|
||||||
|
|
||||||
|
console.log("UI fees - marketAddresses");
|
||||||
|
marketAddresses.forEach(marketAddress => {
|
||||||
|
console.log(marketAddress);
|
||||||
|
});
|
||||||
|
console.log("UI fees - tokenAddresses");
|
||||||
|
tokenAddresses.forEach(tokenAddress => {
|
||||||
|
console.log(tokenAddress);
|
||||||
|
});
|
||||||
|
console.log("UI fees - receiver account", sdk.account);
|
||||||
|
|
||||||
|
// Execute the claim UI fees transaction using sdk.callContract
|
||||||
|
|
||||||
|
|
||||||
|
return "ui_fees_claimed"; // Return a success indicator
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error claiming UI fees:', error);
|
||||||
|
throw new Error(`Failed to claim UI fees: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claims UI fees on GMX
|
||||||
|
* @param this The FastifyRequest instance
|
||||||
|
* @param reply The FastifyReply instance
|
||||||
|
* @param account The wallet address of the user
|
||||||
|
* @returns The response object with success status and transaction hash
|
||||||
|
*/
|
||||||
|
export async function claimGmxUiFees(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
account: string
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Validate the request parameters
|
||||||
|
claimUiFeesSchema.parse({ account });
|
||||||
|
|
||||||
|
// Get client for the address
|
||||||
|
const sdk = await this.getClientForAddress(account);
|
||||||
|
|
||||||
|
// Call the implementation function
|
||||||
|
const hash = await claimGmxUiFeesImpl(sdk);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
hash
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(this, reply, error, 'gmx/claim-ui-fees');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation function to swap tokens on GMX using the SDK's built-in swap method
|
||||||
|
* @param sdk The GMX SDK client
|
||||||
|
* @param fromTicker The ticker symbol of the token to swap from
|
||||||
|
* @param toTicker The ticker symbol of the token to swap to
|
||||||
|
* @param amount The amount to swap
|
||||||
|
* @param orderType The order type (market or limit)
|
||||||
|
* @param triggerRatio The trigger ratio for limit orders (optional)
|
||||||
|
* @param allowedSlippage The allowed slippage percentage (default 0.5%)
|
||||||
|
* @returns The transaction hash
|
||||||
|
*/
|
||||||
|
export const swapGmxTokensImpl = async (
|
||||||
|
sdk: GmxSdk,
|
||||||
|
fromTicker: string,
|
||||||
|
toTicker: string,
|
||||||
|
amount: number,
|
||||||
|
orderType: 'market' | 'limit' = 'market',
|
||||||
|
triggerRatio?: number,
|
||||||
|
allowedSlippage: number = 0.5
|
||||||
|
): Promise<string> => {
|
||||||
|
try {
|
||||||
|
// Get markets and tokens data from GMX SDK with cache
|
||||||
|
const {marketsInfoData, tokensData} = await getMarketsInfoWithCache(sdk);
|
||||||
|
|
||||||
|
if (!marketsInfoData || !tokensData) {
|
||||||
|
throw new Error("No markets or tokens info data");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token data for from and to tokens
|
||||||
|
const fromTokenData = getTokenDataFromTicker(fromTicker, tokensData);
|
||||||
|
const toTokenData = getTokenDataFromTicker(toTicker, tokensData);
|
||||||
|
|
||||||
|
console.log("fromTokenData", fromTokenData);
|
||||||
|
if (!fromTokenData || !toTokenData) {
|
||||||
|
throw new Error(`Token data not found for ${fromTicker} or ${toTicker}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the from token amount with proper decimals
|
||||||
|
const fromTokenAmount = BigInt(Math.floor(amount * Math.pow(10, fromTokenData.decimals)));
|
||||||
|
|
||||||
|
// Check and handle token allowance for ExchangeRouter contract
|
||||||
|
const syntheticsRouterRouterAddress = getContract(sdk.chainId, "SyntheticsRouter");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentAllowance = await getTokenAllowance(
|
||||||
|
sdk.account,
|
||||||
|
fromTokenData.address,
|
||||||
|
syntheticsRouterRouterAddress
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Current allowance for ${fromTicker}:`, currentAllowance.toString());
|
||||||
|
console.log(`Required amount:`, fromTokenAmount.toString());
|
||||||
|
|
||||||
|
if (currentAllowance < fromTokenAmount) {
|
||||||
|
console.log(`🔧 Insufficient allowance for ${fromTicker}. Auto-approving ExchangeRouter...`);
|
||||||
|
|
||||||
|
// Calculate a reasonable approval amount (use the larger of required amount or 1000 tokens)
|
||||||
|
const tokenUnit = BigInt(Math.pow(10, fromTokenData.decimals));
|
||||||
|
const minApprovalAmount = tokenUnit * 1000n; // 1000 tokens
|
||||||
|
const approvalAmount = fromTokenAmount > minApprovalAmount ? fromTokenAmount : minApprovalAmount;
|
||||||
|
|
||||||
|
const approvalHash = await approveContractImpl(
|
||||||
|
sdk.account,
|
||||||
|
fromTokenData.address,
|
||||||
|
syntheticsRouterRouterAddress,
|
||||||
|
sdk.chainId,
|
||||||
|
approvalAmount
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Token approval successful! Hash: ${approvalHash}`);
|
||||||
|
console.log(`📝 Approved ${approvalAmount.toString()} ${fromTicker} for ExchangeRouter`);
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Sufficient allowance already exists for ${fromTicker}`);
|
||||||
|
}
|
||||||
|
} catch (allowanceError) {
|
||||||
|
console.warn('Could not check or approve token allowance:', allowanceError);
|
||||||
|
throw new Error(`Failed to handle token allowance: ${allowanceError instanceof Error ? allowanceError.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate trigger price for limit orders
|
||||||
|
let triggerPrice: bigint | undefined;
|
||||||
|
if (orderType === 'limit' && triggerRatio) {
|
||||||
|
// Convert trigger ratio to price format (30 decimals)
|
||||||
|
triggerPrice = BigInt(Math.floor(triggerRatio * Math.pow(10, 30)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert slippage percentage to basis points
|
||||||
|
const allowedSlippageBps = Math.floor(allowedSlippage * 100); // Convert percentage to basis points
|
||||||
|
|
||||||
|
// Create SwapParams for the SDK
|
||||||
|
const swapParams = {
|
||||||
|
fromAmount: fromTokenAmount,
|
||||||
|
fromTokenAddress: fromTokenData.address,
|
||||||
|
toTokenAddress: toTokenData.address,
|
||||||
|
allowedSlippageBps: allowedSlippageBps,
|
||||||
|
referralCodeForTxn: encodeReferralCode("kaigen_ai"),
|
||||||
|
triggerPrice: triggerPrice, // Only defined for limit orders
|
||||||
|
skipSimulation: true,
|
||||||
|
} as SwapParams;
|
||||||
|
|
||||||
|
console.log("swapParams", swapParams);
|
||||||
|
|
||||||
|
swapParams.marketsInfoData = marketsInfoData;
|
||||||
|
swapParams.tokensData = tokensData;
|
||||||
|
|
||||||
|
|
||||||
|
// Use the SDK's built-in swap method
|
||||||
|
await sdk.orders.swap(swapParams);
|
||||||
|
|
||||||
|
return "swap_order_created";
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error swapping GMX tokens:', error);
|
||||||
|
throw new Error(`Failed to swap tokens: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps tokens on GMX
|
||||||
|
* @param this The FastifyRequest instance
|
||||||
|
* @param reply The FastifyReply instance
|
||||||
|
* @param account The wallet address of the user
|
||||||
|
* @param fromTicker The ticker symbol of the token to swap from
|
||||||
|
* @param toTicker The ticker symbol of the token to swap to
|
||||||
|
* @param amount The amount to swap
|
||||||
|
* @param orderType The order type (market or limit)
|
||||||
|
* @param triggerRatio The trigger ratio for limit orders (optional)
|
||||||
|
* @param allowedSlippage The allowed slippage percentage (default 0.5%)
|
||||||
|
* @returns The response object with success status and transaction hash
|
||||||
|
*/
|
||||||
|
export async function swapGmxTokens(
|
||||||
|
this: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
account: string,
|
||||||
|
fromTicker: string,
|
||||||
|
toTicker: string,
|
||||||
|
amount: number,
|
||||||
|
orderType: 'market' | 'limit' = 'market',
|
||||||
|
triggerRatio?: number,
|
||||||
|
allowedSlippage: number = 0.5
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// Validate the request parameters
|
||||||
|
swapTokensSchema.parse({
|
||||||
|
account,
|
||||||
|
fromTicker,
|
||||||
|
toTicker,
|
||||||
|
amount,
|
||||||
|
orderType,
|
||||||
|
triggerRatio,
|
||||||
|
allowedSlippage
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get client for the address
|
||||||
|
const sdk = await this.getClientForAddress(account);
|
||||||
|
|
||||||
|
// Call the implementation function
|
||||||
|
const hash = await swapGmxTokensImpl(
|
||||||
|
sdk,
|
||||||
|
fromTicker,
|
||||||
|
toTicker,
|
||||||
|
amount,
|
||||||
|
orderType,
|
||||||
|
triggerRatio,
|
||||||
|
allowedSlippage
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
hash,
|
||||||
|
message: `Successfully processed ${fromTicker} to ${toTicker} swap. Any required token approvals were handled automatically.`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Handle approval-specific errors with better messaging
|
||||||
|
if (error instanceof Error && error.message.includes('Failed to handle token allowance')) {
|
||||||
|
reply.status(400);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
errorType: 'APPROVAL_FAILED',
|
||||||
|
suggestion: `Token approval failed. Please ensure you have sufficient funds and try again, or manually approve the ExchangeRouter contract to spend your ${fromTicker} tokens.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleError(this, reply, error, 'gmx/swap-tokens');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks token allowances for GMX contracts
|
||||||
|
* @param account The wallet address
|
||||||
|
* @param tokenAddress The token contract address
|
||||||
|
* @param chainId The chain ID (defaults to Arbitrum)
|
||||||
|
* @returns Object with allowances for different GMX contracts
|
||||||
|
*/
|
||||||
|
export const checkGmxTokenAllowances = async (
|
||||||
|
account: string,
|
||||||
|
tokenAddress: string,
|
||||||
|
chainId: number = ARBITRUM
|
||||||
|
): Promise<{
|
||||||
|
exchangeRouter: bigint;
|
||||||
|
orderVault: bigint;
|
||||||
|
syntheticsRouter: bigint;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const exchangeRouterAddress = getContract(chainId, "ExchangeRouter");
|
||||||
|
const orderVaultAddress = getContract(chainId, "OrderVault");
|
||||||
|
const syntheticsRouterAddress = getContract(chainId, "SyntheticsRouter");
|
||||||
|
|
||||||
|
const [exchangeRouterAllowance, orderVaultAllowance, syntheticsRouterAllowance] = await Promise.all([
|
||||||
|
getTokenAllowance(account, tokenAddress, exchangeRouterAddress),
|
||||||
|
getTokenAllowance(account, tokenAddress, orderVaultAddress),
|
||||||
|
getTokenAllowance(account, tokenAddress, syntheticsRouterAddress)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exchangeRouter: exchangeRouterAllowance,
|
||||||
|
orderVault: orderVaultAllowance,
|
||||||
|
syntheticsRouter: syntheticsRouterAllowance
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking GMX token allowances:', error);
|
||||||
|
throw new Error(`Failed to check token allowances: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user