From 918b97b556ebca6c5300de6ba2dbe574076cd893 Mon Sep 17 00:00:00 2001 From: Mario Date: Mon, 4 Mar 2024 12:44:29 +0100 Subject: [PATCH] feat: Add mana balance to address pages (#1206) * feat: Add mana balance to AddressBalance component. Add mana balance in useAddressBalance state. * feat: Add blockIssuanceCredits to AddressBalance * feat: Add mana rewards to Account address AddressBalance * feat: Add mana from the actual output for Account/Anchor/Nft addresses to Mana Balance * fix: Remove mana rewards for Anchor & Nft addresses * fix: Make balances be column on mobile in AddressBalance --- .../nova/address/AccountAddressView.tsx | 27 +- .../nova/address/AddressBalance.scss | 105 +++++--- .../nova/address/AddressBalance.tsx | 240 ++++++++++++++---- .../nova/address/AnchorAddressView.tsx | 24 +- .../nova/address/Ed25519AddressView.tsx | 24 +- .../ImplicitAccountCreationAddressView.tsx | 16 +- .../nova/address/NftAddressView.tsx | 24 +- .../nova/hooks/useAccountAddressState.ts | 43 +++- .../helpers/nova/hooks/useAccountDetails.ts | 12 +- .../helpers/nova/hooks/useAddressBalance.ts | 62 ++++- .../nova/hooks/useAnchorAddressState.ts | 34 ++- .../helpers/nova/hooks/useAnchorDetails.ts | 12 +- .../nova/hooks/useEd25519AddressState.ts | 36 ++- .../useImplicitAccountCreationAddressState.ts | 23 +- .../helpers/nova/hooks/useNftAddressState.ts | 34 ++- .../src/helpers/nova/hooks/useNftDetails.ts | 12 +- .../nova/address/IAddressBalanceResponse.ts | 2 +- 17 files changed, 534 insertions(+), 196 deletions(-) diff --git a/client/src/app/components/nova/address/AccountAddressView.tsx b/client/src/app/components/nova/address/AccountAddressView.tsx index 524a1cd8b..29ae4acd3 100644 --- a/client/src/app/components/nova/address/AccountAddressView.tsx +++ b/client/src/app/components/nova/address/AccountAddressView.tsx @@ -12,7 +12,17 @@ interface AccountAddressViewProps { const AccountAddressView: React.FC = ({ accountAddress }) => { const [state, setState] = useAccountAddressState(accountAddress); - const { addressDetails, totalBalance, availableBalance, isAccountDetailsLoading, isAssociatedOutputsLoading } = state; + const { + addressDetails, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + congestion, + manaRewards, + isAccountDetailsLoading, + isAssociatedOutputsLoading, + } = state; const isPageLoading = isAccountDetailsLoading || isAssociatedOutputsLoading; return ( @@ -35,13 +45,14 @@ const AccountAddressView: React.FC = ({ accountAddress
- {totalBalance !== null && ( - - )} +
diff --git a/client/src/app/components/nova/address/AddressBalance.scss b/client/src/app/components/nova/address/AddressBalance.scss index ce9177294..4c38a109c 100644 --- a/client/src/app/components/nova/address/AddressBalance.scss +++ b/client/src/app/components/nova/address/AddressBalance.scss @@ -1,75 +1,98 @@ @import "../../../../scss/media-queries"; .balance-wrapper { - margin-top: 40px; + display: flex; + flex-direction: row; + align-items: center; .icon { margin-right: 16px; } - .balance-wrapper--inner { + .balance-wrapper__inner { display: flex; + flex-direction: column; - .balance { + .balance-wrapper__base-token { + margin-bottom: 12px; + } + + .balance-wrapper__base-token, + .balance-wrapper__mana { display: flex; flex-direction: row; + .balance { + display: flex; + flex-direction: column; - .icon { - align-self: center; + .icon { + align-self: center; - @include tablet-down { - display: none; + @include tablet-down { + display: none; + } } - } - &:not(:last-child) { - margin-right: 40px; + &:not(:last-child) { + margin-right: 40px; - @include tablet-down { - margin-right: 0px; + @include tablet-down { + margin-right: 0px; + } } - } - - .balance-value { - display: flex; - flex-direction: column; - @include tablet-down { - flex-direction: row; - } + .balance-value { + display: flex; + flex-direction: column; - .balance-value--inline { @include tablet-down { - margin-left: 5px; + flex-direction: row; + } + + .balance-value--inline { + @include tablet-down { + margin-left: 5px; + } } } - } - .balance-base-token, - .balance-fiat { - color: #b0bfd9; - font-size: 18px; - } + .balance-base-token, + .balance-fiat { + color: #b0bfd9; + font-size: 18px; + } - .balance-heading { - height: 20px; + .balance-heading { + height: 20px; - .material-icons { - font-size: 18px; - color: #b0bfd9; - padding-left: 5px; + .material-icons { + font-size: 18px; + color: #b0bfd9; + padding-left: 5px; + } + } + + .balance__mana { + display: flex; + align-items: center; + margin-left: 8px; + + .value { + margin-left: 4px; + } } } } - } - @include tablet-down { - .balance-wrapper--inner { - flex-direction: column; + @include tablet-down { + .balance-wrapper__base-token, + .balance-wrapper__mana { + flex-direction: column; - .balance { - &:not(:first-child) { - margin-top: 26px; + .balance { + &:not(:first-child) { + margin-top: 8px; + } } } } diff --git a/client/src/app/components/nova/address/AddressBalance.tsx b/client/src/app/components/nova/address/AddressBalance.tsx index 8d7434cb1..02782d1ed 100644 --- a/client/src/app/components/nova/address/AddressBalance.tsx +++ b/client/src/app/components/nova/address/AddressBalance.tsx @@ -1,5 +1,7 @@ +import { INodeInfoBaseToken } from "@iota/sdk-wasm-nova/web"; import React, { useState } from "react"; import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; +import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { formatAmount } from "~helpers/stardust/valueFormatHelper"; import CopyButton from "../../CopyButton"; import Icon from "../../Icon"; @@ -8,55 +10,209 @@ import "./AddressBalance.scss"; interface AddressBalanceProps { /** - * The totalBalance amount from chronicle (representing trivial + conditional balance). + * The base token totalBalance amount from chronicle (representing trivial + conditional balance). */ - readonly totalBalance: number; + readonly totalBaseTokenBalance?: number | null; /** - * The trivially unlockable portion of the balance, fetched from chronicle. + * The trivially unlockable portion of the base token balance (from chronicle). */ - readonly availableBalance: number | null; + readonly availableBaseTokenBalance?: number | null; + /** + * The mana totalBalance amount from chronicle (representing trivial + conditional balance). + */ + readonly totalManaBalance?: IManaBalance | null; + /** + * The trivially unlockable portion of the mana balance (from chronicle). + */ + readonly availableManaBalance?: IManaBalance | null; + /** + * The block issuance credits (for Account addresses). + */ + readonly blockIssuanceCredits?: bigint | null; + /** + * The account mana rewards (for Account addresses). + */ + readonly manaRewards?: bigint | null; /** * The storage rent balance. */ - readonly storageDeposit: number | null; + readonly storageDeposit?: number | null; } const CONDITIONAL_BALANCE_INFO = "These funds reside within outputs with additional unlock conditions which might be potentially un-lockable"; -const AddressBalance: React.FC = ({ totalBalance, availableBalance, storageDeposit }) => { +const AddressBalance: React.FC = ({ + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + blockIssuanceCredits, + manaRewards, + storageDeposit, +}) => { const { tokenInfo } = useNetworkInfoNova((s) => s.networkInfo); const [formatBalanceFull, setFormatBalanceFull] = useState(false); const [formatConditionalBalanceFull, setFormatConditionalBalanceFull] = useState(false); const [formatStorageBalanceFull, setFormatStorageBalanceFull] = useState(false); - const buildBalanceView = ( + if (totalBaseTokenBalance === null) { + return null; + } + + const baseTokenBalanceView = buildBaseTokenBalanceView(tokenInfo); + + const conditionalBaseTokenBalance = + !availableBaseTokenBalance || !totalBaseTokenBalance ? undefined : totalBaseTokenBalance - availableBaseTokenBalance; + const shouldShowExtendedBalance = conditionalBaseTokenBalance !== undefined && availableBaseTokenBalance !== undefined; + + const totalStoredMana = totalManaBalance?.stored ?? null; + const totalPotentialMana = totalManaBalance?.potential ?? null; + const availableStoredMana = availableManaBalance?.stored ?? null; + const availablePotentialMana = availableManaBalance?.potential ?? null; + + const conditionalStoredMana = availableStoredMana === null || totalStoredMana === null ? null : totalStoredMana - availableStoredMana; + const conditionalPotentialMana = + availablePotentialMana === null || totalPotentialMana === null ? null : totalPotentialMana - availablePotentialMana; + + return ( +
+ +
+
+ {baseTokenBalanceView( + "Available Base Token", + formatBalanceFull, + setFormatBalanceFull, + false, + shouldShowExtendedBalance ? availableBaseTokenBalance : totalBaseTokenBalance, + )} + {shouldShowExtendedBalance && + baseTokenBalanceView( + "Conditionally Locked Base Token", + formatConditionalBalanceFull, + setFormatConditionalBalanceFull, + true, + conditionalBaseTokenBalance, + )} + {baseTokenBalanceView("Storage Deposit", formatStorageBalanceFull, setFormatStorageBalanceFull, false, storageDeposit)} +
+ +
+ {(availableStoredMana !== null || availablePotentialMana !== null || blockIssuanceCredits !== null) && + manaBalanceView("Available Mana", availableStoredMana, availablePotentialMana, blockIssuanceCredits, manaRewards)} + {(conditionalStoredMana !== null || conditionalPotentialMana !== null) && + manaBalanceView("Conditionally Locked Mana", conditionalStoredMana, conditionalPotentialMana)} +
+
+
+ ); +}; + +function buildBaseTokenBalanceView(tokenInfo: INodeInfoBaseToken) { + const baseTokenBalanceView = ( label: string, isFormatFull: boolean, setIsFormatFull: React.Dispatch>, showInfo: boolean, - showWallet: boolean, - amount: number | null, + amount?: number | null, ) => (
- {showWallet && } -
-
-
{label}
- {showInfo && ( - - info - +
+
{label}
+ {showInfo && ( + + info + + )} +
+
+ {amount && amount > 0 ? ( +
+
+ setIsFormatFull(!isFormatFull)} className="balance-base-token pointer margin-r-5"> + {formatAmount(amount, tokenInfo, isFormatFull)} + + +
+
+ ) : ( + 0 + )} +
+
+ ); + + 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 + )} +
+
+ {blockIssuanceCredits !== null && ( +
+
Block issuance credits:
+
+ {blockIssuanceCredits && blockIssuanceCredits > 0 ? ( +
+
+ {blockIssuanceCredits.toString()} + +
+
+ ) : ( + 0 )}
+
+ )} + {manaRewards !== null && ( +
+
Mana rewards:
- {amount !== null && amount > 0 ? ( + {manaRewards && manaRewards > 0 ? (
- setIsFormatFull(!isFormatFull)} className="balance-base-token pointer margin-r-5"> - {formatAmount(amount, tokenInfo, isFormatFull)} - - + {manaRewards.toString()} +
) : ( @@ -64,36 +220,18 @@ const AddressBalance: React.FC = ({ totalBalance, available )}
-
- ); - - const conditionalBalance = availableBalance === null ? undefined : totalBalance - availableBalance; - const shouldShowExtendedBalance = conditionalBalance !== undefined && availableBalance !== undefined; + )} +
+); - return ( -
-
- {buildBalanceView( - "Available Balance", - formatBalanceFull, - setFormatBalanceFull, - false, - true, - shouldShowExtendedBalance ? availableBalance : totalBalance, - )} - {shouldShowExtendedBalance && - buildBalanceView( - "Conditionally Locked Balance", - formatConditionalBalanceFull, - setFormatConditionalBalanceFull, - true, - false, - conditionalBalance, - )} - {buildBalanceView("Storage Deposit", formatStorageBalanceFull, setFormatStorageBalanceFull, false, false, storageDeposit)} -
-
- ); +AddressBalance.defaultProps = { + totalBaseTokenBalance: null, + availableBaseTokenBalance: null, + totalManaBalance: null, + availableManaBalance: null, + blockIssuanceCredits: null, + manaRewards: null, + storageDeposit: null, }; export default AddressBalance; diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx index da36bc8f8..31142634f 100644 --- a/client/src/app/components/nova/address/AnchorAddressView.tsx +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -12,7 +12,15 @@ interface AnchorAddressViewProps { const AnchorAddressView: React.FC = ({ anchorAddress }) => { const [state, setState] = useAnchorAddressState(anchorAddress); - const { addressDetails, totalBalance, availableBalance, isAnchorDetailsLoading, isAssociatedOutputsLoading } = state; + const { + addressDetails, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + isAnchorDetailsLoading, + isAssociatedOutputsLoading, + } = state; const isPageLoading = isAnchorDetailsLoading || isAssociatedOutputsLoading; return ( @@ -35,13 +43,13 @@ const AnchorAddressView: React.FC = ({ anchorAddress })
- {totalBalance !== null && ( - - )} +
diff --git a/client/src/app/components/nova/address/Ed25519AddressView.tsx b/client/src/app/components/nova/address/Ed25519AddressView.tsx index 97131eaf1..a53089065 100644 --- a/client/src/app/components/nova/address/Ed25519AddressView.tsx +++ b/client/src/app/components/nova/address/Ed25519AddressView.tsx @@ -12,7 +12,15 @@ interface Ed25519AddressViewProps { const Ed25519AddressView: React.FC = ({ ed25519Address }) => { const [state, setState] = useEd25519AddressState(ed25519Address); - const { addressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading, isBasicOutputsLoading } = state; + const { + addressDetails, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + isAssociatedOutputsLoading, + isBasicOutputsLoading, + } = state; const isPageLoading = isAssociatedOutputsLoading || isBasicOutputsLoading; return ( @@ -35,13 +43,13 @@ const Ed25519AddressView: React.FC = ({ ed25519Address
- {totalBalance !== null && ( - - )} +
diff --git a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx index 2831e064a..13471039d 100644 --- a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx +++ b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx @@ -12,7 +12,7 @@ interface ImplicitAccountCreationAddressViewProps { const ImplicitAccountCreationAddressView: React.FC = ({ implicitAccountCreationAddress }) => { const [state, setState] = useImplicitAccountCreationAddressState(implicitAccountCreationAddress); - const { addressDetails, totalBalance, availableBalance, isAssociatedOutputsLoading } = state; + const { addressDetails, totalBaseTokenBalance, availableBaseTokenBalance, isAssociatedOutputsLoading } = state; const isPageLoading = isAssociatedOutputsLoading; return ( @@ -35,13 +35,13 @@ const ImplicitAccountCreationAddressView: React.FC
- {totalBalance !== null && ( - - )} +
diff --git a/client/src/app/components/nova/address/NftAddressView.tsx b/client/src/app/components/nova/address/NftAddressView.tsx index cb54c241d..b54080591 100644 --- a/client/src/app/components/nova/address/NftAddressView.tsx +++ b/client/src/app/components/nova/address/NftAddressView.tsx @@ -12,7 +12,15 @@ interface NftAddressViewProps { const NftAddressView: React.FC = ({ nftAddress }) => { const [state, setState] = useNftAddressState(nftAddress); - const { addressDetails, totalBalance, availableBalance, isNftDetailsLoading, isAssociatedOutputsLoading } = state; + const { + addressDetails, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + isNftDetailsLoading, + isAssociatedOutputsLoading, + } = state; const isPageLoading = isNftDetailsLoading || isAssociatedOutputsLoading; return ( @@ -35,13 +43,13 @@ const NftAddressView: React.FC = ({ nftAddress }) => {
- {totalBalance !== null && ( - - )} +
diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index a865357df..647cd58fd 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -5,6 +5,7 @@ import { BlockIssuerFeature, CongestionResponse, FeatureType, + ManaRewardsResponse, OutputResponse, StakingFeature, ValidatorResponse, @@ -21,13 +22,18 @@ import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; import { useAccountCongestion } from "./useAccountCongestion"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; import { useAccountValidatorDetails } from "./useAccountValidatorDetails"; +import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; +import { useOutputManaRewards } from "./useOutputManaRewards"; export interface IAccountAddressState { addressDetails: IAddressDetails | null; accountOutput: AccountOutput | null; - totalBalance: number | null; - availableBalance: number | null; + totalBaseTokenBalance: number | null; + availableBaseTokenBalance: number | null; + totalManaBalance: IManaBalance | null; + availableManaBalance: IManaBalance | null; blockIssuerFeature: BlockIssuerFeature | null; + manaRewards: ManaRewardsResponse | null; stakingFeature: StakingFeature | null; validatorDetails: ValidatorResponse | null; addressBasicOutputs: OutputResponse[] | null; @@ -48,9 +54,12 @@ export interface IAccountAddressState { const initialState = { addressDetails: null, accountOutput: null, - totalBalance: null, - availableBalance: null, + totalBaseTokenBalance: null, + availableBaseTokenBalance: null, + totalManaBalance: null, + availableManaBalance: null, blockIssuerFeature: null, + manaRewards: null, stakingFeature: null, validatorDetails: null, addressBasicOutputs: null, @@ -84,9 +93,16 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres initialState, ); - const { accountOutput, isLoading: isAccountDetailsLoading } = useAccountDetails(network, address.accountId); + const { accountOutput, accountOutputMetadata, isLoading: isAccountDetailsLoading } = useAccountDetails(network, address.accountId); + const { manaRewards } = useOutputManaRewards(network, accountOutputMetadata?.outputId ?? ""); - const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, accountOutput); + const { totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, availableManaBalance } = useAddressBalance( + network, + state.addressDetails, + accountOutput, + accountOutputMetadata, + manaRewards, + ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); @@ -112,8 +128,11 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres let updatedState: Partial = { accountOutput, isAccountDetailsLoading, - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + manaRewards, foundries, congestion, validatorDetails, @@ -138,6 +157,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres }; } } + if (!state.stakingFeature) { const stakingFeature = accountOutput?.features?.find((feature) => feature.type === FeatureType.Staking) as StakingFeature; if (stakingFeature) { @@ -152,8 +172,11 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres setState(updatedState); }, [ accountOutput, - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + manaRewards, addressBasicOutputs, addressNftOutputs, congestion, diff --git a/client/src/helpers/nova/hooks/useAccountDetails.ts b/client/src/helpers/nova/hooks/useAccountDetails.ts index db65abc84..ef7608b13 100644 --- a/client/src/helpers/nova/hooks/useAccountDetails.ts +++ b/client/src/helpers/nova/hooks/useAccountDetails.ts @@ -1,4 +1,4 @@ -import { AccountOutput } from "@iota/sdk-wasm-nova/web"; +import { AccountOutput, IOutputMetadataResponse } from "@iota/sdk-wasm-nova/web"; import { useEffect, useState } from "react"; import { ServiceFactory } from "~/factories/serviceFactory"; import { useIsMounted } from "~/helpers/hooks/useIsMounted"; @@ -12,10 +12,14 @@ import { NovaApiClient } from "~/services/nova/novaApiClient"; * @param accountID The account id * @returns The output response and loading bool. */ -export function useAccountDetails(network: string, accountId: string | null): { accountOutput: AccountOutput | null; isLoading: boolean } { +export function useAccountDetails( + network: string, + accountId: string | null, +): { accountOutput: AccountOutput | null; accountOutputMetadata: IOutputMetadataResponse | null; isLoading: boolean } { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const [accountOutput, setAccountOutput] = useState(null); + const [accountOutputMetadata, setAccountOutputMetadata] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -31,8 +35,10 @@ export function useAccountDetails(network: string, accountId: string | null): { .then((response) => { if (!response?.error && isMounted) { const output = response.accountOutputDetails?.output as AccountOutput; + const metadata = response.accountOutputDetails?.metadata ?? null; setAccountOutput(output); + setAccountOutputMetadata(metadata); } }) .finally(() => { @@ -44,5 +50,5 @@ export function useAccountDetails(network: string, accountId: string | null): { } }, [network, accountId]); - return { accountOutput, isLoading }; + return { accountOutput, accountOutputMetadata, isLoading }; } diff --git a/client/src/helpers/nova/hooks/useAddressBalance.ts b/client/src/helpers/nova/hooks/useAddressBalance.ts index 6bbe50a71..e5335cd0f 100644 --- a/client/src/helpers/nova/hooks/useAddressBalance.ts +++ b/client/src/helpers/nova/hooks/useAddressBalance.ts @@ -1,10 +1,13 @@ -import { AddressType, NftOutput, AccountOutput, AnchorOutput } from "@iota/sdk-wasm-nova/web"; +import { AddressType, NftOutput, AccountOutput, AnchorOutput, IOutputMetadataResponse, ManaRewardsResponse } from "@iota/sdk-wasm-nova/web"; import { useEffect, useState } from "react"; +import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { IAddressDetails } from "~/models/api/nova/IAddressDetails"; import { NovaApiClient } from "~/services/nova/novaApiClient"; import { ServiceFactory } from "~factories/serviceFactory"; import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { useNetworkInfoNova } from "~/helpers/nova/networkInfo"; import { NOVA } from "~models/config/protocolVersion"; +import { buildManaDetailsForOutput } from "../manaUtils"; /** * Fetch the address balance from chronicle nova. @@ -17,11 +20,22 @@ export function useAddressBalance( network: string, addressDetails: IAddressDetails | null, output: AccountOutput | NftOutput | AnchorOutput | null, -): { totalBalance: number | null; availableBalance: number | null; isLoading: boolean } { + outputMetadata?: IOutputMetadataResponse | null, + outputManaRewards?: ManaRewardsResponse | null, +): { + totalBaseTokenBalance: number | null; + availableBaseTokenBalance: number | null; + totalManaBalance: IManaBalance | null; + availableManaBalance: IManaBalance | null; + isLoading: boolean; +} { const isMounted = useIsMounted(); + const { protocolInfo, latestConfirmedSlot } = useNetworkInfoNova((s) => s.networkInfo); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); - const [totalBalance, setTotalBalance] = useState(null); - const [availableBalance, setAvailableBalance] = useState(null); + const [totalBaseTokenBalance, setTotalBaseTokenBalance] = useState(null); + const [availableBaseTokenBalance, setAvailableBaseTokenBalance] = useState(null); + const [totalManaBalance, setTotalManaBalance] = useState(null); + const [availableManaBalance, setAvailableManaBalance] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -31,7 +45,7 @@ export function useAddressBalance( addressDetails?.type === AddressType.Account || addressDetails?.type === AddressType.Nft || addressDetails?.type === AddressType.Anchor; - const canLoad = address && (!needsOutputToProceed || (needsOutputToProceed && output !== null)); + const canLoad = address && (!needsOutputToProceed || (needsOutputToProceed && output !== null && outputMetadata)); if (canLoad) { // eslint-disable-next-line no-void @@ -41,14 +55,46 @@ export function useAddressBalance( if (isMounted) { let totalBalance = response?.totalBalance?.amount ?? 0; let availableBalance = response?.availableBalance?.amount ?? 0; + let totalManaBalance = response?.totalBalance?.mana ?? null; + let availableManaBalance = response?.availableBalance?.mana ?? null; if (output) { totalBalance = Number(totalBalance) + Number(output.amount); availableBalance = Number(availableBalance) + Number(output.amount); + + // Output mana + const { included, spent } = outputMetadata ?? {}; + const createdSlotIndex = (included?.slot as number) ?? null; + const spentSlotIndex = (spent?.slot as number) ?? null; + + if (output && createdSlotIndex && protocolInfo) { + const untilSlotIndex = spentSlotIndex ? spentSlotIndex : latestConfirmedSlot > 0 ? latestConfirmedSlot : null; + const outputManaDetails = untilSlotIndex + ? buildManaDetailsForOutput( + output, + createdSlotIndex, + untilSlotIndex, + protocolInfo.parameters, + outputManaRewards ?? null, + ) + : null; + + totalManaBalance = { + stored: (totalManaBalance?.stored ?? 0) + Number(outputManaDetails?.storedMana ?? 0), + potential: (totalManaBalance?.potential ?? 0) + Number(outputManaDetails?.potentialMana ?? 0), + }; + + availableManaBalance = { + stored: (availableManaBalance?.stored ?? 0) + Number(outputManaDetails?.storedMana ?? 0), + potential: (availableManaBalance?.potential ?? 0) + Number(outputManaDetails?.potentialMana ?? 0), + }; + } } - setTotalBalance(totalBalance); - setAvailableBalance(availableBalance > 0 ? availableBalance : null); + setTotalBaseTokenBalance(totalBalance); + setAvailableBaseTokenBalance(availableBalance > 0 ? availableBalance : null); + setTotalManaBalance(totalManaBalance); + setAvailableManaBalance(availableManaBalance); } })(); } else { @@ -56,5 +102,5 @@ export function useAddressBalance( } }, [network, addressDetails, output]); - return { totalBalance, availableBalance, isLoading }; + return { totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, availableManaBalance, isLoading }; } diff --git a/client/src/helpers/nova/hooks/useAnchorAddressState.ts b/client/src/helpers/nova/hooks/useAnchorAddressState.ts index 5fb62255e..8606c6dd1 100644 --- a/client/src/helpers/nova/hooks/useAnchorAddressState.ts +++ b/client/src/helpers/nova/hooks/useAnchorAddressState.ts @@ -9,12 +9,15 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; export interface IAnchorAddressState { addressDetails: IAddressDetails | null; anchorOutput: AnchorOutput | null; - availableBalance: number | null; - totalBalance: number | null; + totalBaseTokenBalance: number | null; + availableBaseTokenBalance: number | null; + totalManaBalance: IManaBalance | null; + availableManaBalance: IManaBalance | null; addressBasicOutputs: OutputResponse[] | null; addressNftOutputs: OutputResponse[] | null; isBasicOutputsLoading: boolean; @@ -28,8 +31,10 @@ export interface IAnchorAddressState { const initialState = { addressDetails: null, anchorOutput: null, - totalBalance: null, - availableBalance: null, + totalBaseTokenBalance: null, + availableBaseTokenBalance: null, + totalManaBalance: null, + availableManaBalance: null, addressBasicOutputs: null, addressNftOutputs: null, isBasicOutputsLoading: false, @@ -56,8 +61,13 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt initialState, ); - const { anchorOutput, isLoading: isAnchorDetailsLoading } = useAnchorDetails(network, address.anchorId); - const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, anchorOutput); + const { anchorOutput, anchorOutputMetadata, isLoading: isAnchorDetailsLoading } = useAnchorDetails(network, address.anchorId); + const { totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, availableManaBalance } = useAddressBalance( + network, + state.addressDetails, + anchorOutput, + anchorOutputMetadata, + ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); @@ -76,8 +86,10 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt useEffect(() => { setState({ anchorOutput, - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, @@ -86,8 +98,10 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt }); }, [ anchorOutput, - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, diff --git a/client/src/helpers/nova/hooks/useAnchorDetails.ts b/client/src/helpers/nova/hooks/useAnchorDetails.ts index 3c88c281a..27cbfe40f 100644 --- a/client/src/helpers/nova/hooks/useAnchorDetails.ts +++ b/client/src/helpers/nova/hooks/useAnchorDetails.ts @@ -1,4 +1,4 @@ -import { AnchorOutput } from "@iota/sdk-wasm-nova/web"; +import { AnchorOutput, IOutputMetadataResponse } from "@iota/sdk-wasm-nova/web"; import { useEffect, useState } from "react"; import { ServiceFactory } from "~/factories/serviceFactory"; import { useIsMounted } from "~/helpers/hooks/useIsMounted"; @@ -12,10 +12,14 @@ import { NovaApiClient } from "~/services/nova/novaApiClient"; * @param anchorID The anchor id * @returns The output response and loading bool. */ -export function useAnchorDetails(network: string, anchorId: string | null): { anchorOutput: AnchorOutput | null; isLoading: boolean } { +export function useAnchorDetails( + network: string, + anchorId: string | null, +): { anchorOutput: AnchorOutput | null; anchorOutputMetadata: IOutputMetadataResponse | null; isLoading: boolean } { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const [anchorOutput, setAnchorOutput] = useState(null); + const [anchorOutputMetadata, setAnchorOutputMetadata] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -31,8 +35,10 @@ export function useAnchorDetails(network: string, anchorId: string | null): { an .then((response) => { if (!response?.error && isMounted) { const output = response.anchorOutputDetails?.output as AnchorOutput; + const metadata = response.anchorOutputDetails?.metadata ?? null; setAnchorOutput(output); + setAnchorOutputMetadata(metadata); } }) .finally(() => { @@ -44,5 +50,5 @@ export function useAnchorDetails(network: string, anchorId: string | null): { an } }, [network, anchorId]); - return { anchorOutput, isLoading }; + return { anchorOutput, anchorOutputMetadata, isLoading }; } diff --git a/client/src/helpers/nova/hooks/useEd25519AddressState.ts b/client/src/helpers/nova/hooks/useEd25519AddressState.ts index 7c9d8fcc7..fd0f6231d 100644 --- a/client/src/helpers/nova/hooks/useEd25519AddressState.ts +++ b/client/src/helpers/nova/hooks/useEd25519AddressState.ts @@ -7,11 +7,14 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; export interface IEd25519AddressState { addressDetails: IAddressDetails | null; - totalBalance: number | null; - availableBalance: number | null; + totalBaseTokenBalance: number | null; + availableBaseTokenBalance: number | null; + totalManaBalance: IManaBalance | null; + availableManaBalance: IManaBalance | null; addressBasicOutputs: OutputResponse[] | null; addressNftOutputs: OutputResponse[] | null; isBasicOutputsLoading: boolean; @@ -23,8 +26,10 @@ export interface IEd25519AddressState { const initialState = { addressDetails: null, - totalBalance: null, - availableBalance: null, + totalBaseTokenBalance: null, + availableBaseTokenBalance: null, + totalManaBalance: null, + availableManaBalance: null, addressBasicOutputs: null, addressNftOutputs: null, isBasicOutputsLoading: false, @@ -49,7 +54,11 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres initialState, ); - const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, null); + const { totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, availableManaBalance } = useAddressBalance( + network, + state.addressDetails, + null, + ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); @@ -66,14 +75,25 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres useEffect(() => { setState({ - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, isNftOutputsLoading, }); - }, [totalBalance, availableBalance, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, isNftOutputsLoading]); + }, [ + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, + addressBasicOutputs, + addressNftOutputs, + isBasicOutputsLoading, + isNftOutputsLoading, + ]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts index e880441e5..92bd8233e 100644 --- a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts +++ b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts @@ -11,8 +11,8 @@ import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs" export interface IImplicitAccountCreationAddressState { addressDetails: IAddressDetails | null; - totalBalance: number | null; - availableBalance: number | null; + totalBaseTokenBalance: number | null; + availableBaseTokenBalance: number | null; addressBasicOutputs: OutputResponse[] | null; addressNftOutputs: OutputResponse[] | null; isBasicOutputsLoading: boolean; @@ -24,8 +24,8 @@ export interface IImplicitAccountCreationAddressState { const initialState = { addressDetails: null, - totalBalance: null, - availableBalance: null, + totalBaseTokenBalance: null, + availableBaseTokenBalance: null, addressBasicOutputs: null, addressNftOutputs: null, isBasicOutputsLoading: false, @@ -53,7 +53,7 @@ export const useImplicitAccountCreationAddressState = ( initialState, ); - const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, null); + const { totalBaseTokenBalance, availableBaseTokenBalance } = useAddressBalance(network, state.addressDetails, null); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); @@ -71,14 +71,21 @@ export const useImplicitAccountCreationAddressState = ( useEffect(() => { setState({ - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, isNftOutputsLoading, }); - }, [totalBalance, availableBalance, addressBasicOutputs, addressNftOutputs, isBasicOutputsLoading, isBasicOutputsLoading]); + }, [ + totalBaseTokenBalance, + availableBaseTokenBalance, + addressBasicOutputs, + addressNftOutputs, + isBasicOutputsLoading, + isBasicOutputsLoading, + ]); return [state, setState]; }; diff --git a/client/src/helpers/nova/hooks/useNftAddressState.ts b/client/src/helpers/nova/hooks/useNftAddressState.ts index 2f09b59b8..0d6024b88 100644 --- a/client/src/helpers/nova/hooks/useNftAddressState.ts +++ b/client/src/helpers/nova/hooks/useNftAddressState.ts @@ -9,12 +9,15 @@ import { AddressHelper } from "~/helpers/nova/addressHelper"; import { useAddressBalance } from "./useAddressBalance"; import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; +import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; export interface INftAddressState { addressDetails: IAddressDetails | null; nftOutput: NftOutput | null; - totalBalance: number | null; - availableBalance: number | null; + totalBaseTokenBalance: number | null; + availableBaseTokenBalance: number | null; + totalManaBalance: IManaBalance | null; + availableManaBalance: IManaBalance | null; addressBasicOutputs: OutputResponse[] | null; addressNftOutputs: OutputResponse[] | null; isBasicOutputsLoading: boolean; @@ -29,8 +32,10 @@ const initialState = { addressDetails: null, nftOutput: null, isNftDetailsLoading: true, - totalBalance: null, - availableBalance: null, + totalBaseTokenBalance: null, + availableBaseTokenBalance: null, + totalManaBalance: null, + availableManaBalance: null, addressBasicOutputs: null, addressNftOutputs: null, isBasicOutputsLoading: false, @@ -56,8 +61,13 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac initialState, ); - const { nftOutput, isLoading: isNftDetailsLoading } = useNftDetails(network, address.nftId); - const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, nftOutput); + const { nftOutput, nftOutputMetadata, isLoading: isNftDetailsLoading } = useNftDetails(network, address.nftId); + const { totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, availableManaBalance } = useAddressBalance( + network, + state.addressDetails, + nftOutput, + nftOutputMetadata, + ); const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); @@ -76,8 +86,10 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac useEffect(() => { setState({ nftOutput, - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, isNftDetailsLoading, addressBasicOutputs, addressNftOutputs, @@ -86,8 +98,10 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac }); }, [ nftOutput, - totalBalance, - availableBalance, + totalBaseTokenBalance, + availableBaseTokenBalance, + totalManaBalance, + availableManaBalance, isNftDetailsLoading, addressBasicOutputs, addressNftOutputs, diff --git a/client/src/helpers/nova/hooks/useNftDetails.ts b/client/src/helpers/nova/hooks/useNftDetails.ts index 9335864d1..5c01252d7 100644 --- a/client/src/helpers/nova/hooks/useNftDetails.ts +++ b/client/src/helpers/nova/hooks/useNftDetails.ts @@ -1,4 +1,4 @@ -import { NftOutput } from "@iota/sdk-wasm-nova/web"; +import { IOutputMetadataResponse, NftOutput } from "@iota/sdk-wasm-nova/web"; import { useEffect, useState } from "react"; import { ServiceFactory } from "~/factories/serviceFactory"; import { useIsMounted } from "~/helpers/hooks/useIsMounted"; @@ -12,10 +12,14 @@ import { NovaApiClient } from "~/services/nova/novaApiClient"; * @param nftID The nft id * @returns The output response and loading bool. */ -export function useNftDetails(network: string, nftId: string | null): { nftOutput: NftOutput | null; isLoading: boolean } { +export function useNftDetails( + network: string, + nftId: string | null, +): { nftOutput: NftOutput | null; nftOutputMetadata: IOutputMetadataResponse | null; isLoading: boolean } { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const [nftOutput, setNftOutput] = useState(null); + const [nftOutputMetadata, setNftOutputMetadata] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -31,8 +35,10 @@ export function useNftDetails(network: string, nftId: string | null): { nftOutpu .then((response) => { if (!response?.error && isMounted) { const output = response.nftOutputDetails?.output as NftOutput; + const metadata = response.nftOutputDetails?.metadata ?? null; setNftOutput(output); + setNftOutputMetadata(metadata); } }) .finally(() => { @@ -44,5 +50,5 @@ export function useNftDetails(network: string, nftId: string | null): { nftOutpu } }, [network, nftId]); - return { nftOutput, isLoading }; + return { nftOutput, nftOutputMetadata, isLoading }; } diff --git a/client/src/models/api/nova/address/IAddressBalanceResponse.ts b/client/src/models/api/nova/address/IAddressBalanceResponse.ts index 36ea92ec1..e1713324d 100644 --- a/client/src/models/api/nova/address/IAddressBalanceResponse.ts +++ b/client/src/models/api/nova/address/IAddressBalanceResponse.ts @@ -1,6 +1,6 @@ import { IResponse } from "../../IResponse"; -interface IManaBalance { +export interface IManaBalance { stored: number; potential: number; }