@@ -369,18 +228,24 @@ export function PositionsSummaryTable({
-
- {(() => {
- if (earnings === null) return '-';
- return (
- formatReadable(
- Number(formatBalance(earnings, groupedPosition.loanAssetDecimals)),
- ) +
- ' ' +
- groupedPosition.loanAsset
- );
- })()}
-
+ {isLoadingEarnings ? (
+
+ ) : (
+
+ {(() => {
+ if (earnings === null) return '-';
+ return (
+ formatReadable(
+ Number(formatBalance(earnings, groupedPosition.loanAssetDecimals)),
+ ) +
+ ' ' +
+ groupedPosition.loanAsset
+ );
+ })()}
+
+ )}
|
diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts
index 2d7c1520..52400872 100644
--- a/src/hooks/usePositionReport.ts
+++ b/src/hooks/usePositionReport.ts
@@ -4,9 +4,9 @@ import {
EarningsCalculation,
filterTransactionsInPeriod,
} from '@/utils/interest';
+import { fetchPositionSnapshot } from '@/utils/positions';
import { estimatedBlockNumber } from '@/utils/rpc';
import { Market, MarketPosition, UserTransaction } from '@/utils/types';
-import { usePositionSnapshot } from './usePositionSnapshot';
import useUserTransactions from './useUserTransactions';
export type PositionReport = {
@@ -38,7 +38,6 @@ export const usePositionReport = (
startDate?: Date,
endDate?: Date,
) => {
- const { fetchPositionSnapshot } = usePositionSnapshot();
const { fetchTransactions } = useUserTransactions();
const generateReport = async (): Promise => {
diff --git a/src/hooks/usePositionSnapshot.ts b/src/hooks/usePositionSnapshot.ts
deleted file mode 100644
index 5b8949b5..00000000
--- a/src/hooks/usePositionSnapshot.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { useCallback } from 'react';
-import { Address } from 'viem';
-
-export type PositionSnapshot = {
- supplyAssets: string;
- supplyShares: string;
- borrowAssets: string;
- borrowShares: string;
- collateral: string;
-};
-
-type PositionResponse = {
- position: {
- supplyAssets: string;
- supplyShares: string;
- borrowAssets: string;
- borrowShares: string;
- collateral: string;
- } | null;
-};
-
-export function usePositionSnapshot() {
- const fetchPositionSnapshot = useCallback(
- async (
- marketId: string,
- userAddress: Address,
- chainId: number,
- blockNumber: number,
- ): Promise => {
- try {
- // Then, fetch the position at that block number
- const positionResponse = await fetch(
- `/api/positions/historical?` +
- `marketId=${encodeURIComponent(marketId)}` +
- `&userAddress=${encodeURIComponent(userAddress)}` +
- `&blockNumber=${encodeURIComponent(blockNumber)}` +
- `&chainId=${encodeURIComponent(chainId)}`,
- );
-
- if (!positionResponse.ok) {
- const errorData = (await positionResponse.json()) as { error?: string };
- console.error('Failed to fetch position snapshot:', errorData);
- return null;
- }
-
- const positionData = (await positionResponse.json()) as PositionResponse;
-
- // If position is empty, return zeros
- if (!positionData.position) {
- return {
- supplyAssets: '0',
- supplyShares: '0',
- borrowAssets: '0',
- borrowShares: '0',
- collateral: '0',
- };
- }
-
- return {
- ...positionData.position,
- };
- } catch (error) {
- console.error('Error fetching position snapshot:', error);
- return null;
- }
- },
- [],
- );
-
- return { fetchPositionSnapshot };
-}
diff --git a/src/hooks/useUserPosition.ts b/src/hooks/useUserPosition.ts
index f4e4e69e..0356a3ff 100644
--- a/src/hooks/useUserPosition.ts
+++ b/src/hooks/useUserPosition.ts
@@ -2,9 +2,9 @@ import { useState, useEffect, useCallback } from 'react';
import { Address } from 'viem';
import { userPositionForMarketQuery } from '@/graphql/queries';
import { SupportedNetworks } from '@/utils/networks';
+import { fetchPositionSnapshot } from '@/utils/positions';
import { MarketPosition } from '@/utils/types';
import { URLS } from '@/utils/urls';
-import { usePositionSnapshot } from './usePositionSnapshot';
const useUserPositions = (
user: string | undefined,
@@ -16,8 +16,6 @@ const useUserPositions = (
const [position, setPosition] = useState(null);
const [positionsError, setPositionsError] = useState(null);
- const { fetchPositionSnapshot } = usePositionSnapshot();
-
const fetchData = useCallback(
async (isRefetch = false, onSuccess?: () => void) => {
if (!user) {
diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts
index 79abeaae..ff269944 100644
--- a/src/hooks/useUserPositions.ts
+++ b/src/hooks/useUserPositions.ts
@@ -4,12 +4,12 @@ import { useState, useEffect, useCallback } from 'react';
import { Address } from 'viem';
import { userPositionsQuery } from '@/graphql/queries';
import { SupportedNetworks } from '@/utils/networks';
+import { fetchPositionSnapshot } from '@/utils/positions';
import { MarketPosition } from '@/utils/types';
import { URLS } from '@/utils/urls';
import { getMarketWarningsWithDetail } from '@/utils/warnings';
import { useUserMarketsCache } from '../hooks/useUserMarketsCache';
import { useMarkets } from './useMarkets';
-import { usePositionSnapshot } from './usePositionSnapshot';
const useUserPositions = (user: string | undefined, showEmpty = false) => {
const [loading, setLoading] = useState(true);
@@ -19,7 +19,6 @@ const useUserPositions = (user: string | undefined, showEmpty = false) => {
const { markets } = useMarkets();
- const { fetchPositionSnapshot } = usePositionSnapshot();
const { getUserMarkets, batchAddUserMarkets } = useUserMarketsCache();
const fetchData = useCallback(
diff --git a/src/hooks/useUserPositionsSummaryData.ts b/src/hooks/useUserPositionsSummaryData.ts
index b5aa6905..264dba01 100644
--- a/src/hooks/useUserPositionsSummaryData.ts
+++ b/src/hooks/useUserPositionsSummaryData.ts
@@ -1,15 +1,12 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { Address } from 'viem';
-import { calculateEarningsFromSnapshot } from '@/utils/interest';
import { SupportedNetworks } from '@/utils/networks';
+import {
+ calculateEarningsFromPeriod as calculateEarnings,
+ initializePositionsWithEmptyEarnings
+} from '@/utils/positions';
import { estimatedBlockNumber } from '@/utils/rpc';
-import {
- MarketPosition,
- MarketPositionWithEarnings,
- PositionEarnings,
- UserTransaction,
-} from '@/utils/types';
-import { usePositionSnapshot } from './usePositionSnapshot';
+import { MarketPositionWithEarnings } from '@/utils/types';
import useUserPositions from './useUserPositions';
import useUserTransactions from './useUserTransactions';
@@ -32,7 +29,6 @@ const useUserPositionsSummaryData = (user: string | undefined) => {
refetch,
} = useUserPositions(user, true);
- const { fetchPositionSnapshot } = usePositionSnapshot();
const { fetchTransactions } = useUserTransactions();
const [positionsWithEarnings, setPositionsWithEarnings] = useState(
@@ -43,8 +39,11 @@ const useUserPositionsSummaryData = (user: string | undefined) => {
const [isLoadingEarnings, setIsLoadingEarnings] = useState(false);
const [error, setError] = useState(null);
- // Loading state that combines all loading states
- const isLoading = positionsLoading || isLoadingBlockNums || isLoadingEarnings;
+ // Loading state for positions that doesn't include earnings calculation
+ const isPositionsLoading = positionsLoading;
+
+ // Loading state that combines all loading states (used for earnings)
+ const isEarningsLoading = isLoadingBlockNums || isLoadingEarnings;
useEffect(() => {
const fetchBlockNums = async () => {
@@ -94,75 +93,15 @@ const useUserPositionsSummaryData = (user: string | undefined) => {
void fetchBlockNums();
}, []);
- const calculateEarningsFromPeriod = useCallback(
- async (
- position: MarketPosition,
- transactions: UserTransaction[],
- userAddress: Address,
- chainId: SupportedNetworks,
- ) => {
- if (!blockNums?.[chainId]) {
- return {
- lifetimeEarned: '0',
- last24hEarned: '0',
- last7dEarned: '0',
- last30dEarned: '0',
- };
- }
-
- const currentBalance = BigInt(position.state.supplyAssets);
- const marketId = position.market.uniqueKey;
- const marketTxs = transactions.filter((tx) => tx.data?.market?.uniqueKey === marketId);
- const now = Math.floor(Date.now() / 1000);
- const blockNum = blockNums[chainId];
-
- const snapshots = await Promise.all([
- fetchPositionSnapshot(marketId, userAddress, chainId, blockNum.day),
- fetchPositionSnapshot(marketId, userAddress, chainId, blockNum.week),
- fetchPositionSnapshot(marketId, userAddress, chainId, blockNum.month),
- ]);
-
- const [snapshot24h, snapshot7d, snapshot30d] = snapshots;
-
- const lifetimeEarnings = calculateEarningsFromSnapshot(currentBalance, 0n, marketTxs, 0, now);
- const last24hEarnings = snapshot24h
- ? calculateEarningsFromSnapshot(
- currentBalance,
- BigInt(snapshot24h.supplyAssets),
- marketTxs,
- now - 24 * 60 * 60,
- now,
- )
- : null;
- const last7dEarnings = snapshot7d
- ? calculateEarningsFromSnapshot(
- currentBalance,
- BigInt(snapshot7d.supplyAssets),
- marketTxs,
- now - 7 * 24 * 60 * 60,
- now,
- )
- : null;
- const last30dEarnings = snapshot30d
- ? calculateEarningsFromSnapshot(
- currentBalance,
- BigInt(snapshot30d.supplyAssets),
- marketTxs,
- now - 30 * 24 * 60 * 60,
- now,
- )
- : null;
-
- return {
- lifetimeEarned: lifetimeEarnings.earned.toString(),
- last24hEarned: last24hEarnings ? last24hEarnings.earned.toString() : null,
- last7dEarned: last7dEarnings ? last7dEarnings.earned.toString() : null,
- last30dEarned: last30dEarnings ? last30dEarnings.earned.toString() : null,
- } as PositionEarnings;
- },
- [fetchPositionSnapshot, blockNums],
- );
+ // Create positions with empty earnings as soon as positions are loaded
+ useEffect(() => {
+ if (positions && positions.length > 0) {
+ // Initialize positions with empty earnings data to display immediately
+ setPositionsWithEarnings(initializePositionsWithEmptyEarnings(positions));
+ }
+ }, [positions]);
+ // Calculate real earnings in the background
useEffect(() => {
const updatePositionsWithEarnings = async () => {
try {
@@ -171,27 +110,43 @@ const useUserPositionsSummaryData = (user: string | undefined) => {
setIsLoadingEarnings(true);
setError(null);
- const positionsWithEarningsData = await Promise.all(
- positions.map(async (position) => {
- const history = await fetchTransactions({
- userAddress: [user],
- marketUniqueKeys: [position.market.uniqueKey],
- });
-
- const earned = await calculateEarningsFromPeriod(
- position,
- history.items,
- user as Address,
- position.market.morphoBlue.chain.id as SupportedNetworks,
+ // Process positions one by one to update earnings progressively
+ // Potential issue: too slow, parallel processing might be better
+ for (const position of positions) {
+ const history = await fetchTransactions({
+ userAddress: [user],
+ marketUniqueKeys: [position.market.uniqueKey],
+ });
+
+ const chainId = position.market.morphoBlue.chain.id as SupportedNetworks;
+ const blockNumbers = blockNums[chainId];
+
+ const earned = await calculateEarnings(
+ position,
+ history.items,
+ user as Address,
+ chainId,
+ blockNumbers
+ );
+
+ // Update this single position with earnings
+ setPositionsWithEarnings(prev => {
+ const updatedPositions = [...prev];
+ const positionIndex = updatedPositions.findIndex(p =>
+ p.market.uniqueKey === position.market.uniqueKey &&
+ p.market.morphoBlue.chain.id === position.market.morphoBlue.chain.id
);
- return {
- ...position,
- earned,
- };
- }),
- );
-
- setPositionsWithEarnings(positionsWithEarningsData);
+
+ if (positionIndex !== -1) {
+ updatedPositions[positionIndex] = {
+ ...updatedPositions[positionIndex],
+ earned,
+ };
+ }
+
+ return updatedPositions;
+ });
+ }
} catch (err) {
setError(err instanceof Error ? err : new Error('Failed to calculate earnings'));
} finally {
@@ -200,11 +155,12 @@ const useUserPositionsSummaryData = (user: string | undefined) => {
};
void updatePositionsWithEarnings();
- }, [positions, user, blockNums, calculateEarningsFromPeriod, fetchTransactions]);
+ }, [positions, user, blockNums, fetchTransactions]);
return {
positions: positionsWithEarnings,
- isLoading,
+ isPositionsLoading, // For initial load of positions only
+ isEarningsLoading, // For earnings calculation
isRefetching,
error: error ?? positionsError,
refetch,
diff --git a/src/utils/positions.ts b/src/utils/positions.ts
new file mode 100644
index 00000000..a8ef8385
--- /dev/null
+++ b/src/utils/positions.ts
@@ -0,0 +1,368 @@
+import { Address } from 'viem';
+import { formatBalance } from './balance';
+import { calculateEarningsFromSnapshot } from './interest';
+import { SupportedNetworks } from './networks';
+import {
+ MarketPosition,
+ MarketPositionWithEarnings,
+ PositionEarnings,
+ UserTransaction,
+ GroupedPosition,
+ WarningWithDetail
+} from './types';
+
+export type PositionSnapshot = {
+ supplyAssets: string;
+ supplyShares: string;
+ borrowAssets: string;
+ borrowShares: string;
+ collateral: string;
+};
+
+type PositionResponse = {
+ position: {
+ supplyAssets: string;
+ supplyShares: string;
+ borrowAssets: string;
+ borrowShares: string;
+ collateral: string;
+ } | null;
+};
+
+/**
+ * Fetches a position snapshot for a specific market, user, and block number
+ *
+ * @param marketId - The unique ID of the market
+ * @param userAddress - The user's address
+ * @param chainId - The chain ID of the network
+ * @param blockNumber - The block number to fetch the position at (0 for latest)
+ * @returns The position snapshot or null if there was an error
+ */
+export async function fetchPositionSnapshot(
+ marketId: string,
+ userAddress: Address,
+ chainId: number,
+ blockNumber: number,
+): Promise {
+ try {
+ // Fetch the position at the specified block number
+ const positionResponse = await fetch(
+ `/api/positions/historical?` +
+ `marketId=${encodeURIComponent(marketId)}` +
+ `&userAddress=${encodeURIComponent(userAddress)}` +
+ `&blockNumber=${encodeURIComponent(blockNumber)}` +
+ `&chainId=${encodeURIComponent(chainId)}`,
+ );
+
+ if (!positionResponse.ok) {
+ const errorData = (await positionResponse.json()) as { error?: string };
+ console.error('Failed to fetch position snapshot:', errorData);
+ return null;
+ }
+
+ const positionData = (await positionResponse.json()) as PositionResponse;
+
+ // If position is empty, return zeros
+ if (!positionData.position) {
+ return {
+ supplyAssets: '0',
+ supplyShares: '0',
+ borrowAssets: '0',
+ borrowShares: '0',
+ collateral: '0',
+ };
+ }
+
+ return {
+ ...positionData.position,
+ };
+ } catch (error) {
+ console.error('Error fetching position snapshot:', error);
+ return null;
+ }
+}
+
+/**
+ * Calculates earnings for a position across different time periods
+ *
+ * @param position - The market position
+ * @param transactions - User transactions for the position
+ * @param userAddress - The user's address
+ * @param chainId - The chain ID
+ * @param blockNumbers - Block numbers for different time periods
+ * @returns Position earnings data
+ */
+export async function calculateEarningsFromPeriod(
+ position: MarketPosition,
+ transactions: UserTransaction[],
+ userAddress: Address,
+ chainId: SupportedNetworks,
+ blockNumbers: { day: number; week: number; month: number },
+): Promise {
+ if (!blockNumbers) {
+ return {
+ lifetimeEarned: '0',
+ last24hEarned: '0',
+ last7dEarned: '0',
+ last30dEarned: '0',
+ };
+ }
+
+ const currentBalance = BigInt(position.state.supplyAssets);
+ const marketId = position.market.uniqueKey;
+ const marketTxs = transactions.filter((tx) => tx.data?.market?.uniqueKey === marketId);
+ const now = Math.floor(Date.now() / 1000);
+
+ const snapshots = await Promise.all([
+ fetchPositionSnapshot(marketId, userAddress, chainId, blockNumbers.day),
+ fetchPositionSnapshot(marketId, userAddress, chainId, blockNumbers.week),
+ fetchPositionSnapshot(marketId, userAddress, chainId, blockNumbers.month),
+ ]);
+
+ const [snapshot24h, snapshot7d, snapshot30d] = snapshots;
+
+ const lifetimeEarnings = calculateEarningsFromSnapshot(currentBalance, 0n, marketTxs, 0, now);
+ const last24hEarnings = snapshot24h
+ ? calculateEarningsFromSnapshot(
+ currentBalance,
+ BigInt(snapshot24h.supplyAssets),
+ marketTxs,
+ now - 24 * 60 * 60,
+ now,
+ )
+ : null;
+ const last7dEarnings = snapshot7d
+ ? calculateEarningsFromSnapshot(
+ currentBalance,
+ BigInt(snapshot7d.supplyAssets),
+ marketTxs,
+ now - 7 * 24 * 60 * 60,
+ now,
+ )
+ : null;
+ const last30dEarnings = snapshot30d
+ ? calculateEarningsFromSnapshot(
+ currentBalance,
+ BigInt(snapshot30d.supplyAssets),
+ marketTxs,
+ now - 30 * 24 * 60 * 60,
+ now,
+ )
+ : null;
+
+ return {
+ lifetimeEarned: lifetimeEarnings.earned.toString(),
+ last24hEarned: last24hEarnings ? last24hEarnings.earned.toString() : null,
+ last7dEarned: last7dEarnings ? last7dEarnings.earned.toString() : null,
+ last30dEarned: last30dEarnings ? last30dEarnings.earned.toString() : null,
+ };
+}
+
+/**
+ * Export enum for earnings period selection
+ */
+export enum EarningsPeriod {
+ All = 'all',
+ Day = '1D',
+ Week = '7D',
+ Month = '30D',
+}
+
+/**
+ * Get the earnings value for a specific period
+ *
+ * @param position - Position with earnings data
+ * @param period - The period to get earnings for
+ * @returns The earnings value as a string
+ */
+export function getEarningsForPeriod(
+ position: MarketPositionWithEarnings,
+ period: EarningsPeriod
+): string | null {
+ if (!position.earned) return '0';
+
+ switch (period) {
+ case EarningsPeriod.All:
+ return position.earned.lifetimeEarned;
+ case EarningsPeriod.Day:
+ return position.earned.last24hEarned;
+ case EarningsPeriod.Week:
+ return position.earned.last7dEarned;
+ case EarningsPeriod.Month:
+ return position.earned.last30dEarned;
+ default:
+ return '0';
+ }
+}
+
+/**
+ * Get combined earnings for a group of positions
+ *
+ * @param groupedPosition - The grouped position
+ * @param period - The period to get earnings for
+ * @returns The total earnings as a string or null
+ */
+export function getGroupedEarnings(
+ groupedPosition: GroupedPosition,
+ period: EarningsPeriod
+): string | null {
+ return (
+ groupedPosition.markets
+ .reduce(
+ (total, position) => {
+ const earnings = getEarningsForPeriod(position, period);
+ if (earnings === null) return null;
+ return total === null ? BigInt(earnings) : total + BigInt(earnings);
+ },
+ null as bigint | null,
+ )
+ ?.toString() ?? null
+ );
+}
+
+/**
+ * Group positions by loan asset
+ *
+ * @param positions - Array of positions with earnings
+ * @param rebalancerInfo - Optional rebalancer info
+ * @returns Array of grouped positions
+ */
+export function groupPositionsByLoanAsset(
+ positions: MarketPositionWithEarnings[],
+ rebalancerInfo?: { marketCaps: { marketId: string }[] }
+): GroupedPosition[] {
+ return positions
+ .filter(
+ (position) =>
+ BigInt(position.state.supplyShares) > 0 ||
+ rebalancerInfo?.marketCaps.some((c) => c.marketId === position.market.uniqueKey),
+ )
+ .reduce((acc: GroupedPosition[], position) => {
+ const loanAssetAddress = position.market.loanAsset.address;
+ const loanAssetDecimals = position.market.loanAsset.decimals;
+ const chainId = position.market.morphoBlue.chain.id;
+
+ let groupedPosition = acc.find(
+ (gp) => gp.loanAssetAddress === loanAssetAddress && gp.chainId === chainId,
+ );
+
+ if (!groupedPosition) {
+ groupedPosition = {
+ loanAsset: position.market.loanAsset.symbol || 'Unknown',
+ loanAssetAddress,
+ loanAssetDecimals,
+ chainId,
+ totalSupply: 0,
+ totalWeightedApy: 0,
+ collaterals: [],
+ markets: [],
+ processedCollaterals: [],
+ allWarnings: [],
+ };
+ acc.push(groupedPosition);
+ }
+
+ // only push if the position has > 0 supply, earning or is in rebalancer info
+ if (
+ Number(position.state.supplyShares) === 0 &&
+ !rebalancerInfo?.marketCaps.some((c) => c.marketId === position.market.uniqueKey)
+ ) {
+ return acc;
+ }
+
+ groupedPosition.markets.push(position);
+
+ groupedPosition.allWarnings = [
+ ...new Set([
+ ...groupedPosition.allWarnings,
+ ...(position.market.warningsWithDetail || []),
+ ]),
+ ] as WarningWithDetail[];
+
+ const supplyAmount = Number(
+ formatBalance(position.state.supplyAssets, position.market.loanAsset.decimals),
+ );
+ groupedPosition.totalSupply += supplyAmount;
+
+ const weightedApy = supplyAmount * position.market.state.supplyApy;
+ groupedPosition.totalWeightedApy += weightedApy;
+
+ const collateralAddress = position.market.collateralAsset?.address;
+ const collateralSymbol = position.market.collateralAsset?.symbol;
+
+ if (collateralAddress && collateralSymbol) {
+ const existingCollateral = groupedPosition.collaterals.find(
+ (c) => c.address === collateralAddress,
+ );
+ if (existingCollateral) {
+ existingCollateral.amount += supplyAmount;
+ } else {
+ groupedPosition.collaterals.push({
+ address: collateralAddress,
+ symbol: collateralSymbol,
+ amount: supplyAmount,
+ });
+ }
+ }
+
+ return acc;
+ }, [])
+ .filter((groupedPosition) => groupedPosition.totalSupply > 0)
+ .sort((a, b) => b.totalSupply - a.totalSupply);
+}
+
+/**
+ * Process collaterals for grouped positions, simplifying small collaterals into an "Others" category
+ *
+ * @param groupedPositions - Array of grouped positions
+ * @returns Processed grouped positions with simplified collaterals
+ */
+export function processCollaterals(groupedPositions: GroupedPosition[]): GroupedPosition[] {
+ return groupedPositions.map((position) => {
+ const sortedCollaterals = [...position.collaterals].sort((a, b) => b.amount - a.amount);
+ const totalSupply = position.totalSupply;
+ const processedCollaterals = [];
+ let othersAmount = 0;
+
+ for (const collateral of sortedCollaterals) {
+ const percentage = (collateral.amount / totalSupply) * 100;
+ if (percentage >= 5) {
+ processedCollaterals.push({ ...collateral, percentage });
+ } else {
+ othersAmount += collateral.amount;
+ }
+ }
+
+ if (othersAmount > 0) {
+ const othersPercentage = (othersAmount / totalSupply) * 100;
+ processedCollaterals.push({
+ address: 'others',
+ symbol: 'Others',
+ amount: othersAmount,
+ percentage: othersPercentage,
+ });
+ }
+
+ return { ...position, processedCollaterals };
+ });
+}
+
+/**
+ * Initialize positions with empty earnings data
+ *
+ * @param positions - Original positions without earnings data
+ * @returns Positions with initialized empty earnings
+ */
+export function initializePositionsWithEmptyEarnings(
+ positions: MarketPosition[]
+): MarketPositionWithEarnings[] {
+ return positions.map(position => ({
+ ...position,
+ earned: {
+ lifetimeEarned: '0',
+ last24hEarned: null,
+ last7dEarned: null,
+ last30dEarned: null,
+ }
+ }));
+}
\ No newline at end of file
|