From f41af96406b482276f9df0f097decf9eb6260800 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Tue, 3 Jun 2025 02:54:48 +0700 Subject: [PATCH] Start claiming rebate --- .../src/plugins/custom/gmx.ts | 157 ++++++++++++++++++ .../src/routes/api/gmx/index.ts | 31 ++++ .../test/plugins/get-gmx-rebate-stats | 62 +++++++ .../test/plugins/get-gmx-rebate-stats.test.ts | 62 +++++++ 4 files changed, 312 insertions(+) create mode 100644 src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats create mode 100644 src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats.test.ts diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 08e5d4c..e1d1486 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -102,6 +102,7 @@ declare module 'fastify' { closeGmxPosition: typeof closeGmxPosition; getGmxTrade: typeof getGmxTrade; getGmxPositions: typeof getGmxPositions; + getGmxRebateStats: typeof getGmxRebateStats; } } @@ -860,6 +861,7 @@ export default fp(async (fastify) => { fastify.decorateRequest('closeGmxPosition', closeGmxPosition) fastify.decorateRequest('getGmxTrade', getGmxTrade) fastify.decorateRequest('getGmxPositions', getGmxPositions) + fastify.decorateRequest('getGmxRebateStats', getGmxRebateStats) // Pre-populate and refresh the markets cache on startup fastify.addHook('onReady', async () => { @@ -872,4 +874,159 @@ 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 { getContract } = await import('../../generated/gmxsdk/configs/contracts.js'); + const { decodeReferralCode } = await import('../../generated/gmxsdk/utils/referrals.js'); + const { basisPointsToFloat } = await import('../../generated/gmxsdk/utils/numbers.js'); + const { zeroHash } = await import('viem'); + + 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'); + } +} diff --git a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts index 9ba4995..d3fa0a8 100644 --- a/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts +++ b/src/Managing.Web3Proxy/src/routes/api/gmx/index.ts @@ -149,6 +149,37 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { account ) }) + + // Define route to get rebate stats + fastify.get('/rebate-stats', { + schema: { + querystring: Type.Object({ + account: Type.String() + }), + response: { + 200: Type.Object({ + success: Type.Boolean(), + rebateStats: Type.Optional(Type.Object({ + totalRebateUsd: Type.Number(), + discountUsd: Type.Number(), + volume: Type.Number(), + tier: Type.Number(), + rebateFactor: Type.Number(), + discountFactor: Type.Number() + })), + error: Type.Optional(Type.String()) + }) + } + } + }, async (request, reply) => { + const { account } = request.query + + // Call the getGmxRebateStats method from the GMX plugin + return request.getGmxRebateStats( + reply, + account + ) + }) } export default plugin \ No newline at end of file diff --git a/src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats b/src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats new file mode 100644 index 0000000..d3e7992 --- /dev/null +++ b/src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats @@ -0,0 +1,62 @@ +import { test } from 'node:test' +import assert from 'node:assert' +import { getClientForAddress, getGmxRebateStatsImpl } from '../../src/plugins/custom/gmx' + +test('GMX get rebate stats', async (t) => { + await t.test('should get rebate stats for account', async () => { + const testAccount = '0x932167388dD9aad41149b3cA23eBD489E2E2DD78' + const sdk = await getClientForAddress(testAccount) + + const result = await getGmxRebateStatsImpl(sdk) + + console.log('Rebate stats result:', result) + assert.ok(result, 'Rebate stats result should be defined') + + // Check that the result has the expected structure + assert.ok(typeof result.totalRebateUsd === 'number', 'totalRebateUsd should be a number') + assert.ok(typeof result.discountUsd === 'number', 'discountUsd should be a number') + assert.ok(typeof result.volume === 'number', 'volume should be a number') + assert.ok(typeof result.tier === 'number', 'tier should be a number') + assert.ok(typeof result.rebateFactor === 'number', 'rebateFactor should be a number') + assert.ok(typeof result.discountFactor === 'number', 'discountFactor should be a number') + + // All values should be non-negative + assert.ok(result.totalRebateUsd >= 0, 'totalRebateUsd should be non-negative') + assert.ok(result.discountUsd >= 0, 'discountUsd should be non-negative') + assert.ok(result.volume >= 0, 'volume should be non-negative') + assert.ok(result.tier >= 0, 'tier should be non-negative') + assert.ok(result.rebateFactor >= 0, 'rebateFactor should be non-negative') + assert.ok(result.discountFactor >= 0, 'discountFactor should be non-negative') + }) + + await t.test('should handle account with no referral info', async () => { + // Test with a different account that might not have referral data + const testAccount = '0x0000000000000000000000000000000000000000' + const sdk = await getClientForAddress(testAccount) + + const result = await getGmxRebateStatsImpl(sdk) + + console.log('Rebate stats result for empty account:', result) + assert.ok(result, 'Rebate stats result should be defined even for empty account') + + // Should return default values for account with no referral info + assert.strictEqual(result.totalRebateUsd, 0, 'totalRebateUsd should be 0 for empty account') + assert.strictEqual(result.discountUsd, 0, 'discountUsd should be 0 for empty account') + assert.strictEqual(result.volume, 0, 'volume should be 0 for empty account') + assert.strictEqual(result.tier, 0, 'tier should be 0 for empty account') + assert.strictEqual(result.rebateFactor, 0, 'rebateFactor should be 0 for empty account') + assert.strictEqual(result.discountFactor, 0, 'discountFactor should be 0 for empty account') + }) + + await t.test('should handle errors gracefully', async () => { + // Test with an invalid account address to trigger error handling + try { + const sdk = await getClientForAddress('invalid-address') + await getGmxRebateStatsImpl(sdk) + assert.fail('Should have thrown an error for invalid address') + } catch (error) { + assert.ok(error instanceof Error, 'Should throw an Error instance') + console.log('Expected error for invalid address:', error.message) + } + }) +}) diff --git a/src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats.test.ts b/src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats.test.ts new file mode 100644 index 0000000..fba188c --- /dev/null +++ b/src/Managing.Web3Proxy/test/plugins/get-gmx-rebate-stats.test.ts @@ -0,0 +1,62 @@ +import {test} from 'node:test' +import assert from 'node:assert' +import {getClientForAddress, getGmxRebateStatsImpl} from '../../src/plugins/custom/gmx.ts' + +test('GMX get rebate stats', async (t) => { + await t.test('should get rebate stats for account', async () => { + const testAccount = '0xbBA4eaA534cbD0EcAed5E2fD6036Aec2E7eE309f' + const sdk = await getClientForAddress(testAccount) + + const result = await getGmxRebateStatsImpl(sdk) + + console.log('Rebate stats result:', result) + assert.ok(result, 'Rebate stats result should be defined') + + // Check that the result has the expected structure + assert.ok(typeof result.totalRebateUsd === 'number', 'totalRebateUsd should be a number') + assert.ok(typeof result.discountUsd === 'number', 'discountUsd should be a number') + assert.ok(typeof result.volume === 'number', 'volume should be a number') + assert.ok(typeof result.tier === 'number', 'tier should be a number') + assert.ok(typeof result.rebateFactor === 'number', 'rebateFactor should be a number') + assert.ok(typeof result.discountFactor === 'number', 'discountFactor should be a number') + + // All values should be non-negative + assert.ok(result.totalRebateUsd >= 0, 'totalRebateUsd should be non-negative') + assert.ok(result.discountUsd >= 0, 'discountUsd should be non-negative') + assert.ok(result.volume >= 0, 'volume should be non-negative') + assert.ok(result.tier >= 0, 'tier should be non-negative') + assert.ok(result.rebateFactor >= 0, 'rebateFactor should be non-negative') + assert.ok(result.discountFactor >= 0, 'discountFactor should be non-negative') + }) + + await t.test('should handle account with no referral info', async () => { + // Test with a different account that might not have referral data + const testAccount = '0x0000000000000000000000000000000000000000' + const sdk = await getClientForAddress(testAccount) + + const result = await getGmxRebateStatsImpl(sdk) + + console.log('Rebate stats result for empty account:', result) + assert.ok(result, 'Rebate stats result should be defined even for empty account') + + // Should return default values for account with no referral info + assert.strictEqual(result.totalRebateUsd, 0, 'totalRebateUsd should be 0 for empty account') + assert.strictEqual(result.discountUsd, 0, 'discountUsd should be 0 for empty account') + assert.strictEqual(result.volume, 0, 'volume should be 0 for empty account') + assert.strictEqual(result.tier, 0, 'tier should be 0 for empty account') + assert.strictEqual(result.rebateFactor, 0, 'rebateFactor should be 0 for empty account') + assert.strictEqual(result.discountFactor, 0, 'discountFactor should be 0 for empty account') + }) + + await t.test('should handle errors gracefully', async () => { + // Test with an invalid account address to trigger error handling + try { + const sdk = await getClientForAddress('invalid-address') + await getGmxRebateStatsImpl(sdk) + assert.fail('Should have thrown an error for invalid address') + } catch (error) { + assert.ok(error instanceof Error, 'Should throw an Error instance') + console.log('Expected error for invalid address:', error.message) + } + }) +}) \ No newline at end of file