diff --git a/client/src/app/AppUtils.tsx b/client/src/app/AppUtils.tsx index a0417264f..ec9d9af97 100644 --- a/client/src/app/AppUtils.tsx +++ b/client/src/app/AppUtils.tsx @@ -7,7 +7,7 @@ import { IOTA_UI, Theme } from "~models/config/uiTheme"; import { IStardustNodeInfo } from "~services/stardust/nodeInfoService"; import { ServiceFactory } from "~/factories/serviceFactory"; import { NodeInfoService as NodeInfoServiceNova } from "~services/nova/nodeInfoService"; -import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; +import { MANA_INFO_DEFAULT, useNetworkInfoNova } from "~/helpers/nova/networkInfo"; import { NavigationRoute } from "./lib/interfaces"; import { InfoResponse } from "@iota/sdk-wasm-nova/web"; @@ -167,6 +167,7 @@ export const populateNetworkInfoNova = (networkName: string) => { setNetworkInfoNova({ name: networkName, tokenInfo: nodeInfo?.baseToken ?? {}, + manaInfo: MANA_INFO_DEFAULT, protocolVersion: protocolInfo?.parameters.version ?? -1, protocolInfo, latestConfirmedSlot: nodeInfo?.status?.latestConfirmedBlockSlot ?? -1, diff --git a/client/src/app/components/nova/address/AddressBalance.tsx b/client/src/app/components/nova/address/AddressBalance.tsx index ec08897c9..3a32fc366 100644 --- a/client/src/app/components/nova/address/AddressBalance.tsx +++ b/client/src/app/components/nova/address/AddressBalance.tsx @@ -51,8 +51,9 @@ const AddressBalance: React.FC = ({ manaRewards, storageDeposit, }) => { - const { tokenInfo } = useNetworkInfoNova((s) => s.networkInfo); - const [formatBalanceFull, setFormatBalanceFull] = useState(false); + const { tokenInfo, manaInfo } = useNetworkInfoNova((s) => s.networkInfo); + const [formatBaseTokenBalanceFull, setFormatBaseTokenBalanceFull] = useState(false); + const [formatManaBalanceFull, setFormatManaBalanceFull] = useState(false); const [formatConditionalBalanceFull, setFormatConditionalBalanceFull] = useState(false); const [formatStorageBalanceFull, setFormatStorageBalanceFull] = useState(false); @@ -61,6 +62,7 @@ const AddressBalance: React.FC = ({ } const baseTokenBalanceView = buildBaseTokenBalanceView(tokenInfo); + const manaBalanceView = buildManaBalanceView(manaInfo); const conditionalBaseTokenBalance = !availableBaseTokenBalance || !totalBaseTokenBalance ? undefined : totalBaseTokenBalance - availableBaseTokenBalance; @@ -82,8 +84,8 @@ const AddressBalance: React.FC = ({
{baseTokenBalanceView( "Available Base Token", - formatBalanceFull, - setFormatBalanceFull, + formatBaseTokenBalanceFull, + setFormatBaseTokenBalanceFull, false, shouldShowExtendedBalance ? availableBaseTokenBalance : totalBaseTokenBalance, )} @@ -100,9 +102,23 @@ const AddressBalance: React.FC = ({
{(availableStoredMana !== null || availablePotentialMana !== null || blockIssuanceCredits !== null) && - manaBalanceView("Available Mana", availableStoredMana, availablePotentialMana, blockIssuanceCredits, manaRewards)} + manaBalanceView( + "Available Mana", + formatManaBalanceFull, + setFormatManaBalanceFull, + availableStoredMana, + availablePotentialMana, + blockIssuanceCredits, + manaRewards, + )} {(conditionalStoredMana !== null || conditionalPotentialMana !== null) && - manaBalanceView("Conditionally Locked Mana", conditionalStoredMana, conditionalPotentialMana)} + manaBalanceView( + "Conditionally Locked Mana", + formatStorageBalanceFull, + setFormatStorageBalanceFull, + conditionalStoredMana, + conditionalPotentialMana, + )}
@@ -146,56 +162,30 @@ function buildBaseTokenBalanceView(tokenInfo: BaseTokenResponse) { return baseTokenBalanceView; } -const manaBalanceView = ( - label: string, - storedMana: number | null, - potentialMana: number | null, - blockIssuanceCredits: bigint | null = null, - manaRewards: bigint | null = null, -) => ( -
-
-
{label}
-
-
-
Stored:
-
- {storedMana !== null && storedMana > 0 ? ( -
-
- {storedMana} - -
-
- ) : ( - 0 - )} -
-
-
-
Potential:
-
- {potentialMana !== null && potentialMana > 0 ? ( -
-
- {potentialMana} - -
-
- ) : ( - 0 - )} +function buildManaBalanceView(manaInfo: BaseTokenResponse) { + const manaTokenBalanceView = ( + label: string, + isFormatFull: boolean, + setIsFormatFull: React.Dispatch>, + storedMana: number | null, + potentialMana: number | null, + blockIssuanceCredits: bigint | null = null, + manaRewards: bigint | null = null, + ) => ( +
+
+
{label}
-
- {blockIssuanceCredits !== null && (
-
Block issuance credits:
+
Stored:
- {blockIssuanceCredits && blockIssuanceCredits > 0 ? ( + {storedMana !== null && storedMana > 0 ? (
- {blockIssuanceCredits.toString()} - + setIsFormatFull(!isFormatFull)}> + {formatAmount(storedMana, manaInfo, isFormatFull)} + +
) : ( @@ -203,16 +193,16 @@ const manaBalanceView = ( )}
- )} - {manaRewards !== null && (
-
Mana rewards:
+
Potential:
- {manaRewards && manaRewards > 0 ? ( + {potentialMana !== null && potentialMana > 0 ? (
- {manaRewards.toString()} - + setIsFormatFull(!isFormatFull)}> + {formatAmount(potentialMana, manaInfo, isFormatFull)} + +
) : ( @@ -220,9 +210,49 @@ const manaBalanceView = ( )}
- )} -
-); + {blockIssuanceCredits !== null && ( +
+
Block issuance credits:
+
+ {blockIssuanceCredits && blockIssuanceCredits > 0 ? ( +
+
+ setIsFormatFull(!isFormatFull)}> + {formatAmount(blockIssuanceCredits.toString(), manaInfo, isFormatFull)} + + +
+
+ ) : ( + 0 + )} +
+
+ )} + {manaRewards !== null && ( +
+
Mana rewards:
+
+ {manaRewards && manaRewards > 0 ? ( +
+
+ setIsFormatFull(!isFormatFull)}> + {formatAmount(manaRewards.toString(), manaInfo, isFormatFull)} + + +
+
+ ) : ( + 0 + )} +
+
+ )} +
+ ); + + return manaTokenBalanceView; +} AddressBalance.defaultProps = { totalBaseTokenBalance: null, diff --git a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx index 4cdcba92f..56856c45e 100644 --- a/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx +++ b/client/src/app/components/stardust/statistics/InfluxChartsTab.tsx @@ -35,8 +35,9 @@ export const InfluxChartsTab: React.FC = () => { analyticStats, ] = useChartsState(); - const lockedStorageDepositValue = - formatAmount(Number(analyticStats?.lockedStorageDeposit), tokenInfo).replace(COMMAS_REGEX, ",") ?? "-"; + const lockedStorageDepositValue = analyticStats?.lockedStorageDeposit + ? formatAmount(analyticStats.lockedStorageDeposit, tokenInfo).replace(COMMAS_REGEX, ",") + : "-"; const ids = idGenerator(); diff --git a/client/src/app/routes/stardust/landing/ShimmerClaimedUtils.ts b/client/src/app/routes/stardust/landing/ShimmerClaimedUtils.ts index 912eb98dd..50ac730cc 100644 --- a/client/src/app/routes/stardust/landing/ShimmerClaimedUtils.ts +++ b/client/src/app/routes/stardust/landing/ShimmerClaimedUtils.ts @@ -11,7 +11,7 @@ export const buildShimmerClaimedStats = (claimed: string, supply: string, tokenI const formatFull = bigInt < Math.pow(10, tokenInfo.decimals - 3); const decimals = bigInt < Math.pow(10, tokenInfo.decimals) ? 3 : bigInt < Math.pow(10, tokenInfo.decimals + 2) ? 2 : 0; - claimedFinal = formatAmount(Number(claimedFinal), tokenInfo, formatFull, decimals); + claimedFinal = formatAmount(claimedFinal, tokenInfo, formatFull, decimals); claimedFinal = claimedFinal.replaceAll(COMMAS_REGEX, ","); const claimedPercentBd = new BigDecimal("100", 2).multiply(claimedBd.toString()).divide(supply); diff --git a/client/src/helpers/nova/networkInfo.ts b/client/src/helpers/nova/networkInfo.ts index 78228100a..3fd8a6476 100644 --- a/client/src/helpers/nova/networkInfo.ts +++ b/client/src/helpers/nova/networkInfo.ts @@ -1,9 +1,19 @@ import { BaseTokenResponse, ProtocolParametersResponse } from "@iota/sdk-wasm-nova/web"; import { create } from "zustand"; +export const MANA_INFO_DEFAULT = { + name: "Mana", + tickerSymbol: "Mana", + unit: "Mana", + decimals: 6, + subunit: "µMana", + useMetricPrefix: false, +}; + interface INetworkInfo { name: string; tokenInfo: BaseTokenResponse; + manaInfo: BaseTokenResponse; protocolVersion: number; protocolInfo: ProtocolParametersResponse | null; latestConfirmedSlot: number; @@ -26,6 +36,7 @@ export const useNetworkInfoNova = create((set) => ({ subunit: undefined, useMetricPrefix: true, }, + manaInfo: MANA_INFO_DEFAULT, protocolVersion: -1, protocolInfo: null, latestConfirmedSlot: -1, diff --git a/client/src/helpers/stardust/valueFormatHelper.spec.ts b/client/src/helpers/stardust/valueFormatHelper.spec.ts index e820c2e5a..d64913fef 100644 --- a/client/src/helpers/stardust/valueFormatHelper.spec.ts +++ b/client/src/helpers/stardust/valueFormatHelper.spec.ts @@ -9,58 +9,208 @@ const tokenInfo = { useMetricPrefix: false, }; -test("formatAmount should format 1 subunit properly", () => { - expect(formatAmount(1, tokenInfo)).toBe("0.000001 IOTA"); -}); +describe("formatAmount", () => { + describe("with number values", () => { + test("should format 1 subunit properly", () => { + expect(formatAmount(1, tokenInfo)).toBe("0.000001 IOTA"); + }); -test("formatAmount should format 10 subunit properly", () => { - expect(formatAmount(10, tokenInfo)).toBe("0.00001 IOTA"); -}); + test("should format 10 subunit properly", () => { + expect(formatAmount(10, tokenInfo)).toBe("0.00001 IOTA"); + }); -test("formatAmount should format 100 subunit properly", () => { - expect(formatAmount(100, tokenInfo)).toBe("0.0001 IOTA"); -}); + test("should format 100 subunit properly", () => { + expect(formatAmount(100, tokenInfo)).toBe("0.0001 IOTA"); + }); -test("formatAmount should format 1000 subunit properly", () => { - expect(formatAmount(1000, tokenInfo)).toBe("0.001 IOTA"); -}); + test("should format 1000 subunit properly", () => { + expect(formatAmount(1000, tokenInfo)).toBe("0.001 IOTA"); + }); -test("formatAmount should format 10000 subunit properly", () => { - expect(formatAmount(10000, tokenInfo)).toBe("0.01 IOTA"); -}); + test("should format 10000 subunit properly", () => { + expect(formatAmount(10000, tokenInfo)).toBe("0.01 IOTA"); + }); -test("formatAmount should format 100000 subunit properly", () => { - expect(formatAmount(100000, tokenInfo)).toBe("0.1 IOTA"); -}); + test("should format 100000 subunit properly", () => { + expect(formatAmount(100000, tokenInfo)).toBe("0.1 IOTA"); + }); -test("formatAmount should format 1 unit properly", () => { - expect(formatAmount(1000000, tokenInfo)).toBe("1 IOTA"); -}); + test("should format 1 unit properly", () => { + expect(formatAmount(1000000, tokenInfo)).toBe("1 IOTA"); + }); -test("formatAmount should format 1 unit with fraction properly", () => { - expect(formatAmount(1234567, tokenInfo)).toBe("1.23 IOTA"); -}); + test("should format 1 unit with fraction properly", () => { + expect(formatAmount(1234567, tokenInfo)).toBe("1.23 IOTA"); + }); -test("formatAmount should format 1 unit with trailing decimals properly", () => { - expect(formatAmount(1000000, tokenInfo, false, 2, true)).toBe("1.00 IOTA"); -}); + test("should format 1 unit with trailing decimals properly", () => { + expect(formatAmount(1000000, tokenInfo, false, 2, true)).toBe("1.00 IOTA"); + }); -test("formatAmount should handle edge case from issue 'explorer/issues/822'", () => { - expect(formatAmount(1140000, tokenInfo)).toBe("1.14 IOTA"); -}); + test("should handle edge case from issue 'explorer/issues/822'", () => { + expect(formatAmount(1140000, tokenInfo)).toBe("1.14 IOTA"); + }); -test("formatAmount should honour precision 3", () => { - expect(formatAmount(9999, tokenInfo, false, 3)).toBe("0.009 IOTA"); -}); + test("should honour precision 3", () => { + expect(formatAmount(9999, tokenInfo, false, 3)).toBe("0.009 IOTA"); + }); -test("formatAmount should honour precision 4", () => { - expect(formatAmount(9999, tokenInfo, false, 4)).toBe("0.0099 IOTA"); -}); + test("should honour precision 4", () => { + expect(formatAmount(9999, tokenInfo, false, 4)).toBe("0.0099 IOTA"); + }); -test("formatAmount should honour precision 0", () => { - expect(formatAmount(1450896407249092, tokenInfo, false, 0)).toBe("1450896407 IOTA"); -}); + test("should honour precision 0", () => { + expect(formatAmount(1450896407249092, tokenInfo, false, 0)).toBe("1450896407 IOTA"); + }); + + test("should format big values properly", () => { + expect(formatAmount(1450896407249092, tokenInfo)).toBe("1450896407.24 IOTA"); + }); + + test("should honour format full (number)", () => { + expect(formatAmount(1, tokenInfo, true)).toBe("1 micro"); + }); + }); + + describe("with bigint values", () => { + test("should format 1 subunit properly", () => { + expect(formatAmount(1n, tokenInfo)).toBe("0.000001 IOTA"); + }); + + test("should format 10 subunit properly", () => { + expect(formatAmount(10n, tokenInfo)).toBe("0.00001 IOTA"); + }); + + test("should format 100 subunit properly", () => { + expect(formatAmount(100n, tokenInfo)).toBe("0.0001 IOTA"); + }); + + test("should format 1000 subunit properly", () => { + expect(formatAmount(1000n, tokenInfo)).toBe("0.001 IOTA"); + }); + + test("should format 10000 subunit properly", () => { + expect(formatAmount(10000n, tokenInfo)).toBe("0.01 IOTA"); + }); + + test("should format 100000 subunit properly", () => { + expect(formatAmount(100000n, tokenInfo)).toBe("0.1 IOTA"); + }); + + test("should format 1 unit properly", () => { + expect(formatAmount(1000000n, tokenInfo)).toBe("1 IOTA"); + }); + + test("should format 1 unit with fraction properly", () => { + expect(formatAmount(1234567n, tokenInfo)).toBe("1.23 IOTA"); + }); + + test("should format 1 unit with trailing decimals properly", () => { + expect(formatAmount(1000000n, tokenInfo, false, 2, true)).toBe("1.00 IOTA"); + }); + + test("should handle edge case from issue 'explorer/issues/822'", () => { + expect(formatAmount(1140000n, tokenInfo)).toBe("1.14 IOTA"); + }); + + test("should honour precision 3", () => { + expect(formatAmount(9999n, tokenInfo, false, 3)).toBe("0.009 IOTA"); + }); + + test("should honour precision 4", () => { + expect(formatAmount(9999n, tokenInfo, false, 4)).toBe("0.0099 IOTA"); + }); + + test("should honour precision 0", () => { + expect(formatAmount(1450896407249092n, tokenInfo, false, 0)).toBe("1450896407 IOTA"); + }); + + test("should format big values properly", () => { + expect(formatAmount(1450896407249092n, tokenInfo)).toBe("1450896407.24 IOTA"); + }); + + test("should honour format full (bigint)", () => { + expect(formatAmount(1n, tokenInfo, true)).toBe("1 micro"); + }); + }); + + describe("with string values", () => { + test("should format 1 subunit properly", () => { + expect(formatAmount("1", tokenInfo)).toBe("0.000001 IOTA"); + }); + + test("should format 10 subunit properly", () => { + expect(formatAmount("10", tokenInfo)).toBe("0.00001 IOTA"); + }); + + test("should format 100 subunit properly", () => { + expect(formatAmount("100", tokenInfo)).toBe("0.0001 IOTA"); + }); + + test("should format 1000 subunit properly", () => { + expect(formatAmount("1000", tokenInfo)).toBe("0.001 IOTA"); + }); + + test("should format 10000 subunit properly", () => { + expect(formatAmount("10000", tokenInfo)).toBe("0.01 IOTA"); + }); + + test("should format 100000 subunit properly", () => { + expect(formatAmount("100000", tokenInfo)).toBe("0.1 IOTA"); + }); + + test("should format 1 unit properly", () => { + expect(formatAmount("1000000", tokenInfo)).toBe("1 IOTA"); + }); + + test("should format 1 unit with fraction properly", () => { + expect(formatAmount("1234567", tokenInfo)).toBe("1.23 IOTA"); + }); + + test("should format 1 unit with trailing decimals properly", () => { + expect(formatAmount("1000000", tokenInfo, false, 2, true)).toBe("1.00 IOTA"); + }); + + test("should handle edge case from issue 'explorer/issues/822'", () => { + expect(formatAmount("1140000", tokenInfo)).toBe("1.14 IOTA"); + }); + + test("should honour precision 3", () => { + expect(formatAmount("9999", tokenInfo, false, 3)).toBe("0.009 IOTA"); + }); + + test("should honour precision 4", () => { + expect(formatAmount("9999", tokenInfo, false, 4)).toBe("0.0099 IOTA"); + }); + + test("should honour precision 0", () => { + expect(formatAmount("1450896407249092", tokenInfo, false, 0)).toBe("1450896407 IOTA"); + }); + + test("should format big values properly", () => { + expect(formatAmount("1450896407249092", tokenInfo)).toBe("1450896407.24 IOTA"); + }); + + test("should honour format full (number)", () => { + expect(formatAmount("1", tokenInfo, true)).toBe("1 micro"); + }); + }); + + describe("with undefined values", () => { + test("should not break with Number undefined", () => { + expect(formatAmount(Number(undefined), tokenInfo)).toBe(""); + }); + + test("should not break with Number null", () => { + expect(formatAmount(Number(null), tokenInfo)).toBe(""); + }); + + test("should not break with String undefined", () => { + expect(formatAmount(String(undefined), tokenInfo)).toBe(""); + }); -test("formatAmount should format big values properly", () => { - expect(formatAmount(1450896407249092, tokenInfo)).toBe("1450896407.24 IOTA"); + test("should not break with String null", () => { + expect(formatAmount(String(null), tokenInfo)).toBe(""); + }); + }); }); diff --git a/client/src/helpers/stardust/valueFormatHelper.tsx b/client/src/helpers/stardust/valueFormatHelper.tsx index df66be4f3..87dd2c574 100644 --- a/client/src/helpers/stardust/valueFormatHelper.tsx +++ b/client/src/helpers/stardust/valueFormatHelper.tsx @@ -1,7 +1,7 @@ -import { UnitsHelper } from "@iota/iota.js"; import { INodeInfoBaseToken } from "@iota/sdk-wasm/web"; import React from "react"; import Tooltip from "~app/components/Tooltip"; +import BigDecimal from "../bigDecimal"; /** * The id of the Genesis block. @@ -17,22 +17,29 @@ const GENESIS_BLOCK_ID = "0x0000000000000000000000000000000000000000000000000000 * @returns The formatted string. */ export function formatAmount( - value: number, + value: number | bigint | string, tokenInfo: INodeInfoBaseToken, formatFull: boolean = false, decimalPlaces: number = 2, trailingDecimals?: boolean, ): string { + if (!value || value === "null" || value === "undefined") { + return ""; + } + if (formatFull) { return `${value} ${tokenInfo.subunit ?? tokenInfo.unit}`; } - const baseTokenValue = value / Math.pow(10, tokenInfo.decimals); - const formattedAmount = toFixedNoRound(baseTokenValue, decimalPlaces, trailingDecimals); + const valueBigDecimal = + typeof value === "string" + ? new BigDecimal(value, tokenInfo.decimals, false) + : new BigDecimal(value.toString(), tokenInfo.decimals, false); + + const baseTokenValue = valueBigDecimal.divide(Math.pow(10, tokenInfo.decimals).toString()); + const formattedAmount = toFixedNoRound(baseTokenValue.toString(), decimalPlaces, trailingDecimals); - // useMetricPrefix is broken cause it passes a float value to formatBest - const amount = tokenInfo.useMetricPrefix ? UnitsHelper.formatBest(baseTokenValue) : `${formattedAmount} `; - return `${amount}${tokenInfo.unit}`; + return `${formattedAmount} ${tokenInfo.unit}`; } /** @@ -50,7 +57,7 @@ export function formatNumberWithCommas(value: bigint): string { * @param precision The decimal places to show. * @returns The formatted amount. */ -function toFixedNoRound(value: number, precision: number = 2, trailingDecimals?: boolean): string { +function toFixedNoRound(value: number | string, precision: number = 2, trailingDecimals?: boolean): string { const defaultDecimals = "0".repeat(precision); const valueString = `${value}`; const [integer, fraction = defaultDecimals] = valueString.split(".");