diff --git a/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts b/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts index 42d7f3881d..10b6b53ddd 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts +++ b/packages/apps/dashboard/client/src/features/searchResults/model/addressDetailsSchema.ts @@ -38,23 +38,11 @@ export type AddressDetailsWallet = z.infer; const escrowSchema = z.object({ chainId: z.number().optional().nullable(), address: z.string().optional().nullable(), - balance: z - .string() - .optional() - .nullable() - .transform(transformOptionalTokenAmount), + balance: z.string().optional().nullable(), token: z.string().optional().nullable(), factoryAddress: z.string().optional().nullable(), - totalFundedAmount: z - .string() - .optional() - .nullable() - .transform(transformOptionalTokenAmount), - amountPaid: z - .string() - .optional() - .nullable() - .transform(transformOptionalTokenAmount), + totalFundedAmount: z.string().optional().nullable(), + amountPaid: z.string().optional().nullable(), status: z.string().optional().nullable(), manifest: z.string().optional().nullable(), launcher: z.string().optional().nullable(), @@ -62,6 +50,8 @@ const escrowSchema = z.object({ recordingOracle: z.string().optional().nullable(), reputationOracle: z.string().optional().nullable(), finalResultsUrl: z.string().nullable(), + tokenSymbol: z.string().optional().nullable(), + tokenDecimals: z.number().optional().nullable(), }); export type AddressDetailsEscrow = z.infer; diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx index 8801d5d77a..5270129696 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/EscrowAddress.tsx @@ -10,6 +10,7 @@ import { AddressDetailsEscrow } from '../model/addressDetailsSchema'; import HmtBalance from './HmtBalance'; import TitleSectionWrapper from './TitleSectionWrapper'; +import TokenAmount from './TokenAmount'; type Props = { data: AddressDetailsEscrow; @@ -17,7 +18,6 @@ type Props = { const EscrowAddress: FC = ({ data }) => { const { - token, balance, factoryAddress, totalFundedAmount, @@ -27,16 +27,26 @@ const EscrowAddress: FC = ({ data }) => { exchangeOracle, recordingOracle, reputationOracle, + tokenSymbol, } = data; + const isHmt = tokenSymbol === 'HMT'; return ( - {token} + {tokenSymbol} {balance !== undefined && balance !== null ? ( - + {isHmt ? ( + + ) : ( + + )} ) : null} = ({ data }) => { {factoryAddress} - - {totalFundedAmount} - - HMT - - + - - {amountPaid} - - HMT - - + diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx index aa4f482b53..0d9d00c134 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/HmtBalance.tsx @@ -4,16 +4,15 @@ import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; import useHmtPrice from '@/shared/api/useHmtPrice'; -import { useIsMobile } from '@/shared/hooks/useBreakpoints'; -import FormattedNumber from '@/shared/ui/FormattedNumber'; + +import TokenAmount from './TokenAmount'; type Props = { - balance?: number | null; + balance?: number | string | null; }; const HmtBalance: FC = ({ balance }) => { const { data, isError, isPending } = useHmtPrice(); - const isMobile = useIsMobile(); if (isError) { return N/A; @@ -29,16 +28,9 @@ const HmtBalance: FC = ({ balance }) => { return ( - - - - - {`HMT($${balanceInDollars})`} + + + {`($${balanceInDollars})`} ); diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx index 449d26c9ea..8731f44cfc 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/StakeInfo.tsx @@ -3,43 +3,21 @@ import { FC } from 'react'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { useIsMobile } from '@/shared/hooks/useBreakpoints'; -import FormattedNumber from '@/shared/ui/FormattedNumber'; import SectionWrapper from '@/shared/ui/SectionWrapper'; +import TokenAmount from './TokenAmount'; + type Props = { amountStaked?: number | null; amountLocked?: number | null; amountWithdrawable?: number | null; }; -const renderAmount = (amount: number | null | undefined, isMobile: boolean) => { - return ( - - - - - - HMT - - - ); -}; - const StakeInfo: FC = ({ amountStaked, amountLocked, amountWithdrawable, }) => { - const isMobile = useIsMobile(); if (!amountStaked && !amountLocked && !amountWithdrawable) return null; return ( @@ -53,7 +31,7 @@ const StakeInfo: FC = ({ Staked Tokens - {renderAmount(amountStaked, isMobile)} + )} {Number.isFinite(amountLocked) && ( @@ -61,7 +39,7 @@ const StakeInfo: FC = ({ Locked Tokens - {renderAmount(amountLocked, isMobile)} + )} {Number.isFinite(amountWithdrawable) && ( @@ -69,7 +47,7 @@ const StakeInfo: FC = ({ Withdrawable Tokens - {renderAmount(amountWithdrawable, isMobile)} + )} diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx new file mode 100644 index 0000000000..8d57079fb1 --- /dev/null +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/TokenAmount.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react'; + +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +import { useIsMobile } from '@/shared/hooks/useBreakpoints'; +import FormattedNumber from '@/shared/ui/FormattedNumber'; + +type Props = { + amount: number | string | null | undefined; + tokenSymbol?: string | null | undefined; + alreadyParsed?: boolean; +}; + +const TokenAmount: FC = ({ + amount, + tokenSymbol = 'HMT', + alreadyParsed = false, +}) => { + const isMobile = useIsMobile(); + + return ( + + + + + + {tokenSymbol} + + + ); +}; + +export default TokenAmount; diff --git a/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx b/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx index 5275c53c20..bc74edf214 100644 --- a/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx +++ b/packages/apps/dashboard/client/src/features/searchResults/ui/WalletAddress.tsx @@ -3,8 +3,6 @@ import { FC } from 'react'; import Stack from '@mui/material/Stack'; import Typography from '@mui/material/Typography'; -import { useIsMobile } from '@/shared/hooks/useBreakpoints'; -import FormattedNumber from '@/shared/ui/FormattedNumber'; import SectionWrapper from '@/shared/ui/SectionWrapper'; import { @@ -18,6 +16,7 @@ import KVStore from './KvStore'; import ReputationScore from './ReputationScore'; import StakeInfo from './StakeInfo'; import TitleSectionWrapper from './TitleSectionWrapper'; +import TokenAmount from './TokenAmount'; type Props = { data: AddressDetailsWallet | AddressDetailsOperator; @@ -31,7 +30,6 @@ const WalletAddress: FC = ({ data }) => { reputation, amountWithdrawable, } = data; - const isMobile = useIsMobile(); const isWallet = 'totalHMTAmountReceived' in data; return ( @@ -56,20 +54,7 @@ const WalletAddress: FC = ({ data }) => { title="Earned Payouts" tooltip="Total amount earned by participating in jobs" > - - - - - HMT - + )} diff --git a/packages/apps/dashboard/server/src/modules/details/details.service.ts b/packages/apps/dashboard/server/src/modules/details/details.service.ts index 361e674c10..1be4efe63b 100644 --- a/packages/apps/dashboard/server/src/modules/details/details.service.ts +++ b/packages/apps/dashboard/server/src/modules/details/details.service.ts @@ -1,5 +1,11 @@ import { plainToInstance } from 'class-transformer'; -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + Logger, + Inject, +} from '@nestjs/common'; +import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { ChainId, EscrowUtils, @@ -39,6 +45,7 @@ import { KVStoreDataDto } from './dto/details-response.dto'; export class DetailsService { private readonly logger = new Logger(DetailsService.name); constructor( + @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, private readonly configService: EnvironmentConfigService, private readonly httpService: HttpService, private readonly networkConfig: NetworkConfigService, @@ -48,18 +55,37 @@ export class DetailsService { chainId: ChainId, address: string, ): Promise { + const network = this.networkConfig.networks.find( + (network) => network.chainId === chainId, + ); + if (!network) throw new BadRequestException('Invalid chainId provided'); + const provider = new ethers.JsonRpcProvider(network.rpcUrl); + const escrowData = await EscrowUtils.getEscrow(chainId, address); if (escrowData) { const escrowDto: EscrowDto = plainToInstance(EscrowDto, escrowData, { excludeExtraneousValues: true, }); + + const { decimals, symbol } = await this.getTokenData( + chainId, + escrowData.token, + provider, + ); + + escrowDto.balance = ethers.formatUnits(escrowData.balance, decimals); + escrowDto.totalFundedAmount = ethers.formatUnits( + escrowData.totalFundedAmount, + decimals, + ); + escrowDto.amountPaid = ethers.formatUnits( + escrowData.amountPaid, + decimals, + ); + escrowDto.tokenSymbol = symbol; + escrowDto.tokenDecimals = decimals; return escrowDto; } - const network = this.networkConfig.networks.find( - (network) => network.chainId === chainId, - ); - if (!network) throw new BadRequestException('Invalid chainId provided'); - const provider = new ethers.JsonRpcProvider(network.rpcUrl); const stakingClient = await StakingClient.build(provider); const stakingData = await stakingClient.getStakerInfo(address); @@ -111,7 +137,10 @@ export class DetailsService { return walletDto; } - private async getHmtBalance(chainId: ChainId, hmtAddress: string) { + private async getHmtBalance( + chainId: ChainId, + hmtAddress: string, + ): Promise { const network = this.networkConfig.networks.find( (network) => network.chainId === chainId, ); @@ -403,4 +432,26 @@ export class DetailsService { return data; } + + private async getTokenData( + chainId: ChainId, + tokenAddress: string, + provider: ethers.JsonRpcProvider, + ): Promise<{ decimals: number; symbol: string }> { + const tokenCacheKey = `token:${chainId}:${tokenAddress.toLowerCase()}`; + let data = await this.cacheManager.get<{ + decimals: number; + symbol: string; + }>(tokenCacheKey); + if (!data) { + const erc20Contract = HMToken__factory.connect(tokenAddress, provider); + const [decimals, symbol] = await Promise.all([ + erc20Contract.decimals(), + erc20Contract.symbol(), + ]); + data = { decimals: Number(decimals), symbol }; + await this.cacheManager.set(tokenCacheKey, data); + } + return data; + } } diff --git a/packages/apps/dashboard/server/src/modules/details/details.spec.ts b/packages/apps/dashboard/server/src/modules/details/details.spec.ts index ead21997de..8fdfdc2b5f 100644 --- a/packages/apps/dashboard/server/src/modules/details/details.spec.ts +++ b/packages/apps/dashboard/server/src/modules/details/details.spec.ts @@ -13,6 +13,7 @@ import { OrderDirection, } from '@human-protocol/sdk'; import { OperatorsOrderBy } from '../../common/enums/operator'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; jest.mock('@human-protocol/sdk', () => ({ ...jest.requireActual('@human-protocol/sdk'), @@ -57,6 +58,13 @@ describe('DetailsService', () => { .mockResolvedValue([ChainId.MAINNET]), }, }, + { + provide: CACHE_MANAGER, + useValue: { + get: jest.fn(), + set: jest.fn(), + }, + }, Logger, ], }).compile(); diff --git a/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts b/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts index 4f48910aa5..69e8e7bc00 100644 --- a/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts +++ b/packages/apps/dashboard/server/src/modules/details/dto/escrow.dto.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsEnum, IsString, IsUrl } from 'class-validator'; +import { IsEnum, IsNumber, IsString, IsUrl } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { ChainId } from '@human-protocol/sdk'; @@ -73,6 +73,16 @@ export class EscrowDto { @IsUrl() @Expose() public finalResultsUrl?: string; + + @ApiProperty({ example: 'HMT' }) + @IsString() + @Expose() + public tokenSymbol: string; + + @ApiProperty({ example: 18 }) + @IsNumber() + @Expose() + public tokenDecimals: number; } export class EscrowPaginationDto {