diff --git a/api/src/routes.ts b/api/src/routes.ts index b6419cdcb..268c492e5 100644 --- a/api/src/routes.ts +++ b/api/src/routes.ts @@ -241,6 +241,12 @@ export const routes: IRoute[] = [ folder: "nova/address/outputs/nft", func: "get", }, + { + path: "/nova/address/outputs/anchor/:network/:address", + method: "get", + folder: "nova/address/outputs/anchor", + func: "get", + }, { path: "/nova/address/outputs/delegation/:network/:address", method: "get", diff --git a/api/src/routes/nova/address/outputs/anchor/get.ts b/api/src/routes/nova/address/outputs/anchor/get.ts new file mode 100644 index 000000000..3d442377e --- /dev/null +++ b/api/src/routes/nova/address/outputs/anchor/get.ts @@ -0,0 +1,30 @@ +import { ServiceFactory } from "../../../../../factories/serviceFactory"; +import { IAddressDetailsRequest } from "../../../../../models/api/nova/IAddressDetailsRequest"; +import { IAddressDetailsResponse } from "../../../../../models/api/nova/IAddressDetailsResponse"; +import { IConfiguration } from "../../../../../models/configuration/IConfiguration"; +import { NOVA } from "../../../../../models/db/protocolVersion"; +import { NetworkService } from "../../../../../services/networkService"; +import { NovaApiService } from "../../../../../services/nova/novaApiService"; +import { ValidationHelper } from "../../../../../utils/validationHelper"; + +/** + * Fetch the anchor output details by address. + * @param config The configuration. + * @param request The request. + * @returns The response. + */ +export async function get(config: IConfiguration, request: IAddressDetailsRequest): Promise { + const networkService = ServiceFactory.get("network"); + const networks = networkService.networkNames(); + ValidationHelper.oneOf(request.network, networks, "network"); + ValidationHelper.string(request.address, "address"); + + const networkConfig = networkService.get(request.network); + + if (networkConfig.protocolVersion !== NOVA) { + return {}; + } + + const novaApiService = ServiceFactory.get(`api-service-${networkConfig.network}`); + return novaApiService.anchorOutputDetailsByAddress(request.address); +} diff --git a/api/src/services/nova/novaApiService.ts b/api/src/services/nova/novaApiService.ts index 9b96b6cb9..b5f3587d4 100644 --- a/api/src/services/nova/novaApiService.ts +++ b/api/src/services/nova/novaApiService.ts @@ -314,6 +314,33 @@ export class NovaApiService { } } + /** + * Get the relevant anchor output details for an address. + * @param addressBech32 The address in bech32 format. + * @returns The anchor output details. + */ + public async anchorOutputDetailsByAddress(addressBech32: string): Promise { + let cursor: string | undefined; + let outputIds: string[] = []; + + do { + try { + const outputIdsResponse = await this.client.anchorOutputIds({ stateController: addressBech32, cursor: cursor ?? "" }); + + outputIds = outputIds.concat(outputIdsResponse.items); + cursor = outputIdsResponse.cursor; + } catch (e) { + logger.error(`Fetching anchor output ids failed. Cause: ${e}`); + } + } while (cursor); + + const outputResponses = await this.outputsDetails(outputIds); + + return { + outputs: outputResponses, + }; + } + /** * Get the relevant basic output details for an address. * @param addressBech32 The address in bech32 format. @@ -368,9 +395,9 @@ export class NovaApiService { } /** - * Get the relevant basic output details for an address. + * Get the relevant delegation output details for an address. * @param addressBech32 The address in bech32 format. - * @returns The basic output details. + * @returns The delegation output details. */ public async delegationOutputDetailsByAddress(addressBech32: string): Promise { let cursor: string | undefined; diff --git a/client/src/app/components/nova/address/AccountAddressView.tsx b/client/src/app/components/nova/address/AccountAddressView.tsx index e04aa1efd..36c0374f3 100644 --- a/client/src/app/components/nova/address/AccountAddressView.tsx +++ b/client/src/app/components/nova/address/AccountAddressView.tsx @@ -14,6 +14,7 @@ const AccountAddressView: React.FC = ({ accountAddress const [state, setState] = useAccountAddressState(accountAddress); const { addressDetails, + storageDeposit, totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, @@ -51,6 +52,7 @@ const AccountAddressView: React.FC = ({ accountAddress totalManaBalance={totalManaBalance} availableManaBalance={availableManaBalance} blockIssuanceCredits={congestion?.blockIssuanceCredits} + storageDeposit={storageDeposit} manaRewards={manaRewards} /> diff --git a/client/src/app/components/nova/address/AnchorAddressView.tsx b/client/src/app/components/nova/address/AnchorAddressView.tsx index b22c938d6..63669a8a9 100644 --- a/client/src/app/components/nova/address/AnchorAddressView.tsx +++ b/client/src/app/components/nova/address/AnchorAddressView.tsx @@ -14,6 +14,7 @@ const AnchorAddressView: React.FC = ({ anchorAddress }) const [state, setState] = useAnchorAddressState(anchorAddress); const { addressDetails, + storageDeposit, totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, @@ -49,7 +50,7 @@ const AnchorAddressView: React.FC = ({ anchorAddress }) availableBaseTokenBalance={availableBaseTokenBalance} totalManaBalance={totalManaBalance} availableManaBalance={availableManaBalance} - storageDeposit={null} + storageDeposit={storageDeposit} manaRewards={manaRewards} /> diff --git a/client/src/app/components/nova/address/Ed25519AddressView.tsx b/client/src/app/components/nova/address/Ed25519AddressView.tsx index 15b79bfab..21b34887c 100644 --- a/client/src/app/components/nova/address/Ed25519AddressView.tsx +++ b/client/src/app/components/nova/address/Ed25519AddressView.tsx @@ -14,6 +14,7 @@ const Ed25519AddressView: React.FC = ({ ed25519Address const [state, setState] = useEd25519AddressState(ed25519Address); const { addressDetails, + storageDeposit, totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, @@ -49,7 +50,7 @@ const Ed25519AddressView: React.FC = ({ ed25519Address availableBaseTokenBalance={availableBaseTokenBalance} totalManaBalance={totalManaBalance} availableManaBalance={availableManaBalance} - storageDeposit={null} + storageDeposit={storageDeposit} manaRewards={manaRewards} /> diff --git a/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx b/client/src/app/components/nova/address/ImplicitAccountCreationAddressView.tsx index 13471039d..737ece9c8 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, totalBaseTokenBalance, availableBaseTokenBalance, isAssociatedOutputsLoading } = state; + const { addressDetails, storageDeposit, totalBaseTokenBalance, availableBaseTokenBalance, isAssociatedOutputsLoading } = state; const isPageLoading = isAssociatedOutputsLoading; return ( @@ -40,7 +40,7 @@ const ImplicitAccountCreationAddressView: React.FC diff --git a/client/src/app/components/nova/address/NftAddressView.tsx b/client/src/app/components/nova/address/NftAddressView.tsx index 1344103bf..4a4261eb6 100644 --- a/client/src/app/components/nova/address/NftAddressView.tsx +++ b/client/src/app/components/nova/address/NftAddressView.tsx @@ -14,6 +14,7 @@ const NftAddressView: React.FC = ({ nftAddress }) => { const [state, setState] = useNftAddressState(nftAddress); const { addressDetails, + storageDeposit, totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, @@ -49,7 +50,7 @@ const NftAddressView: React.FC = ({ nftAddress }) => { availableBaseTokenBalance={availableBaseTokenBalance} totalManaBalance={totalManaBalance} availableManaBalance={availableManaBalance} - storageDeposit={null} + storageDeposit={storageDeposit} manaRewards={manaRewards} /> diff --git a/client/src/helpers/nova/hooks/useAccountAddressState.ts b/client/src/helpers/nova/hooks/useAccountAddressState.ts index 8ff80714e..f0ba266bb 100644 --- a/client/src/helpers/nova/hooks/useAccountAddressState.ts +++ b/client/src/helpers/nova/hooks/useAccountAddressState.ts @@ -21,6 +21,7 @@ import { useAccountControlledFoundries } from "./useAccountControlledFoundries"; import { useAccountCongestion } from "./useAccountCongestion"; import { useAddressNftOutputs } from "~/helpers/nova/hooks/useAddressNftOutputs"; import { useAccountValidatorDetails } from "./useAccountValidatorDetails"; +import { TransactionsHelper } from "../transactionsHelper"; import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { useOutputManaRewards } from "./useOutputManaRewards"; @@ -29,6 +30,7 @@ import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails export interface IAccountAddressState { addressDetails: IAddressDetails | null; accountOutput: AccountOutput | null; + storageDeposit: number | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; @@ -57,6 +59,7 @@ export interface IAccountAddressState { const initialState = { addressDetails: null, accountOutput: null, + storageDeposit: null, totalBaseTokenBalance: null, availableBaseTokenBalance: null, totalManaBalance: null, @@ -92,7 +95,7 @@ interface IAddressPageLocationProps { export const useAccountAddressState = (address: AccountAddress): [IAccountAddressState, React.Dispatch>] => { const location = useLocation(); const { network } = useParams(); - const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const { bech32Hrp, protocolInfo } = useNetworkInfoNova((s) => s.networkInfo); const [state, setState] = useReducer>>( (currentState, newState) => ({ ...currentState, ...newState }), initialState, @@ -103,11 +106,11 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null); const [addressNftOutputs, isNftOutputsLoading] = useAddressNftOutputs(network, state.addressDetails?.bech32 ?? null); + const [foundries, accountFoundryOutputs, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); const [addressDelegationOutputs, isDelegationOutputsLoading] = useAddressDelegationOutputs( network, state.addressDetails?.bech32 ?? null, ); - const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails); const { congestion, isLoading: isCongestionLoading } = useAccountCongestion(network, state.addressDetails?.hex ?? null); const { validatorDetails, isLoading: isValidatorDetailsLoading } = useAccountValidatorDetails( network, @@ -164,6 +167,20 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres }; if (accountOutput) { + const addressOutputs = [...(addressBasicOutputs ?? []), ...(addressNftOutputs ?? []), ...(accountFoundryOutputs ?? [])].map( + ({ output }) => output, + ); + if (protocolInfo?.parameters.storageScoreParameters) { + const storageDeposit = TransactionsHelper.computeStorageDeposit( + [...addressOutputs, accountOutput], + protocolInfo?.parameters.storageScoreParameters, + ); + updatedState = { + ...updatedState, + storageDeposit, + }; + } + if (!state.blockIssuerFeature) { const blockIssuerFeature = accountOutput?.features?.find( (feature) => feature.type === FeatureType.BlockIssuer, @@ -197,6 +214,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres manaRewards, addressBasicOutputs, addressNftOutputs, + accountFoundryOutputs, addressDelegationOutputs, congestion, validatorDetails, diff --git a/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts b/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts index 32b7b93c6..1de331c67 100644 --- a/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts +++ b/client/src/helpers/nova/hooks/useAccountControlledFoundries.ts @@ -1,4 +1,4 @@ -import { OutputType, HexEncodedString, FoundryOutput, Utils } from "@iota/sdk-wasm-nova/web"; +import { OutputType, HexEncodedString, FoundryOutput, Utils, OutputResponse } from "@iota/sdk-wasm-nova/web"; import { useEffect, useState } from "react"; import { useIsMounted } from "~helpers/hooks/useIsMounted"; import { ServiceFactory } from "~factories/serviceFactory"; @@ -12,16 +12,21 @@ import { NovaApiClient } from "~/services/nova/novaApiClient"; * @param accountAddress The account address * @returns The account foundries and loading bool. */ -export function useAccountControlledFoundries(network: string, accountAddress: IAddressDetails | null): [string[] | null, boolean] { +export function useAccountControlledFoundries( + network: string, + accountAddress: IAddressDetails | null, +): [string[] | null, OutputResponse[] | null, boolean] { const isMounted = useIsMounted(); const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); const [accountFoundries, setAccountFoundries] = useState(null); + const [accountFoundryOutputs, setAccountFoundryOutputs] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { setIsLoading(true); if (accountAddress) { const foundries: string[] = []; + const outputResponses: OutputResponse[] = []; // eslint-disable-next-line no-void void (async () => { apiClient @@ -32,13 +37,15 @@ export function useAccountControlledFoundries(network: string, accountAddress: I .then(async (foundryOutputs) => { if (foundryOutputs?.foundryOutputsResponse && foundryOutputs?.foundryOutputsResponse?.items.length > 0) { for (const foundryOutputId of foundryOutputs.foundryOutputsResponse.items) { - const foundryId = await fetchFoundryId(foundryOutputId); + const { outputDetails, foundryId } = await fetchOutputDetailsAndFoundryId(foundryOutputId); if (foundryId) { foundries.push(foundryId); + outputResponses.push(outputDetails); } } if (isMounted) { setAccountFoundries(foundries); + setAccountFoundryOutputs(outputResponses); } } }) @@ -51,20 +58,19 @@ export function useAccountControlledFoundries(network: string, accountAddress: I } }, [network, accountAddress]); - const fetchFoundryId = async (outputId: HexEncodedString) => { - const foundryId = apiClient.outputDetails({ network, outputId }).then((response) => { - const details = response.output; - if (accountAddress?.hex && !response.error && details?.output?.type === OutputType.Foundry) { - const output = details.output as FoundryOutput; - const serialNumber = output.serialNumber; - const tokenSchemeType = output.tokenScheme.type; - const tokenId = Utils.computeTokenId(accountAddress.hex, serialNumber, tokenSchemeType); + const fetchOutputDetailsAndFoundryId = async (outputId: HexEncodedString) => { + const response = await apiClient.outputDetails({ network, outputId }); + const details = response.output; + if (accountAddress?.hex && !response.error && details?.output?.type === OutputType.Foundry) { + const output = details.output as FoundryOutput; + const serialNumber = output.serialNumber; + const tokenSchemeType = output.tokenScheme.type; + const tokenId = Utils.computeTokenId(accountAddress.hex, serialNumber, tokenSchemeType); - return tokenId; - } - }); - return foundryId; + return { outputDetails: details, foundryId: tokenId }; + } + return { outputDetails: null, foundryId: null }; }; - return [accountFoundries, isLoading]; + return [accountFoundries, accountFoundryOutputs, isLoading]; } diff --git a/client/src/helpers/nova/hooks/useAddressAnchorOutputs.ts b/client/src/helpers/nova/hooks/useAddressAnchorOutputs.ts new file mode 100644 index 000000000..208d3e958 --- /dev/null +++ b/client/src/helpers/nova/hooks/useAddressAnchorOutputs.ts @@ -0,0 +1,43 @@ +import { OutputResponse } from "@iota/sdk-wasm-nova/web"; +import { useEffect, useState } from "react"; +import { useIsMounted } from "~helpers/hooks/useIsMounted"; +import { ServiceFactory } from "~factories/serviceFactory"; +import { NOVA } from "~models/config/protocolVersion"; +import { NovaApiClient } from "~/services/nova/novaApiClient"; + +/** + * Fetch Address anchor UTXOs + * @param network The Network in context + * @param addressBech32 The address in bech32 format + * @returns The output responses and loading bool. + */ +export function useAddressAnchorOutputs(network: string, addressBech32: string | null): [OutputResponse[] | null, boolean] { + const isMounted = useIsMounted(); + const [apiClient] = useState(ServiceFactory.get(`api-client-${NOVA}`)); + const [outputs, setOutputs] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + setIsLoading(true); + setOutputs(null); + if (addressBech32) { + // eslint-disable-next-line no-void + void (async () => { + apiClient + .basicOutputsDetails({ network, address: addressBech32 }) + .then((response) => { + if (!response?.error && response.outputs && isMounted) { + setOutputs(response.outputs); + } + }) + .finally(() => { + setIsLoading(false); + }); + })(); + } else { + setIsLoading(false); + } + }, [network, addressBech32]); + + return [outputs, isLoading]; +} diff --git a/client/src/helpers/nova/hooks/useAnchorAddressState.ts b/client/src/helpers/nova/hooks/useAnchorAddressState.ts index d7687c638..1bb79f29e 100644 --- a/client/src/helpers/nova/hooks/useAnchorAddressState.ts +++ b/client/src/helpers/nova/hooks/useAnchorAddressState.ts @@ -9,6 +9,7 @@ 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 { TransactionsHelper } from "../transactionsHelper"; import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; @@ -16,6 +17,7 @@ import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails export interface IAnchorAddressState { addressDetails: IAddressDetails | null; anchorOutput: AnchorOutput | null; + storageDeposit: number | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; @@ -36,6 +38,7 @@ export interface IAnchorAddressState { const initialState = { addressDetails: null, anchorOutput: null, + storageDeposit: null, totalBaseTokenBalance: null, availableBaseTokenBalance: null, totalManaBalance: null, @@ -63,7 +66,7 @@ interface IAddressPageLocationProps { export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressState, React.Dispatch>] => { const location = useLocation(); const { network } = useParams(); - const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const { bech32Hrp, protocolInfo } = useNetworkInfoNova((s) => s.networkInfo); const [state, setState] = useReducer>>( (currentState, newState) => ({ ...currentState, ...newState }), initialState, @@ -103,7 +106,7 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt }, [address.anchorId]); useEffect(() => { - setState({ + let updatedState: Partial = { anchorOutput, totalBaseTokenBalance, availableBaseTokenBalance, @@ -117,7 +120,23 @@ export const useAnchorAddressState = (address: AnchorAddress): [IAnchorAddressSt isNftOutputsLoading, isDelegationOutputsLoading, isAnchorDetailsLoading, - }); + }; + + if (anchorOutput) { + const addressOutputs = [...(addressBasicOutputs ?? []), ...(addressNftOutputs ?? [])].map(({ output }) => output); + if (protocolInfo?.parameters.storageScoreParameters) { + const storageDeposit = TransactionsHelper.computeStorageDeposit( + [...addressOutputs, anchorOutput], + protocolInfo?.parameters.storageScoreParameters, + ); + updatedState = { + ...updatedState, + storageDeposit, + }; + } + } + + setState(updatedState); }, [ anchorOutput, totalBaseTokenBalance, diff --git a/client/src/helpers/nova/hooks/useEd25519AddressState.ts b/client/src/helpers/nova/hooks/useEd25519AddressState.ts index 4967d4443..21b3de503 100644 --- a/client/src/helpers/nova/hooks/useEd25519AddressState.ts +++ b/client/src/helpers/nova/hooks/useEd25519AddressState.ts @@ -7,12 +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 { TransactionsHelper } from "../transactionsHelper"; import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface IEd25519AddressState { addressDetails: IAddressDetails | null; + storageDeposit: number | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; @@ -31,6 +33,7 @@ export interface IEd25519AddressState { const initialState = { addressDetails: null, + storageDeposit: null, totalBaseTokenBalance: null, availableBaseTokenBalance: null, totalManaBalance: null, @@ -56,7 +59,7 @@ interface IAddressPageLocationProps { export const useEd25519AddressState = (address: Ed25519Address): [IEd25519AddressState, React.Dispatch>] => { const location = useLocation(); - const { name: network, bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const { name: network, bech32Hrp, protocolInfo } = useNetworkInfoNova((s) => s.networkInfo); const [state, setState] = useReducer>>( (currentState, newState) => ({ ...currentState, ...newState }), initialState, @@ -96,7 +99,7 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres }, [address.pubKeyHash]); useEffect(() => { - setState({ + let updatedState: Partial = { totalBaseTokenBalance, availableBaseTokenBalance, totalManaBalance, @@ -108,7 +111,21 @@ export const useEd25519AddressState = (address: Ed25519Address): [IEd25519Addres isBasicOutputsLoading, isNftOutputsLoading, isDelegationOutputsLoading, - }); + }; + + const addressOutputs = [...(addressBasicOutputs ?? []), ...(addressNftOutputs ?? [])].map(({ output }) => output); + if (protocolInfo?.parameters.storageScoreParameters) { + const storageDeposit = TransactionsHelper.computeStorageDeposit( + [...addressOutputs], + protocolInfo?.parameters.storageScoreParameters, + ); + updatedState = { + ...updatedState, + storageDeposit, + }; + } + + setState(updatedState); }, [ totalManaBalance, availableManaBalance, diff --git a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts index f4238e5d7..50f25f6da 100644 --- a/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts +++ b/client/src/helpers/nova/hooks/useImplicitAccountCreationAddressState.ts @@ -8,11 +8,13 @@ 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 { TransactionsHelper } from "../transactionsHelper"; import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; export interface IImplicitAccountCreationAddressState { addressDetails: IAddressDetails | null; + storageDeposit: number | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; addressBasicOutputs: OutputWithMetadataResponse[] | null; @@ -30,6 +32,7 @@ const initialState = { addressDetails: null, totalBaseTokenBalance: null, availableBaseTokenBalance: null, + storageDeposit: null, addressBasicOutputs: null, addressNftOutputs: null, addressDelegationOutputs: null, @@ -53,7 +56,7 @@ export const useImplicitAccountCreationAddressState = ( ): [IImplicitAccountCreationAddressState, React.Dispatch>] => { const location = useLocation(); const { network } = useParams(); - const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const { bech32Hrp, protocolInfo } = useNetworkInfoNova((s) => s.networkInfo); const [state, setState] = useReducer>>( (currentState, newState) => ({ ...currentState, ...newState }), initialState, @@ -80,7 +83,7 @@ export const useImplicitAccountCreationAddressState = ( }, [address.address().pubKeyHash]); useEffect(() => { - setState({ + let updatedState: Partial = { totalBaseTokenBalance, availableBaseTokenBalance, addressBasicOutputs, @@ -89,7 +92,21 @@ export const useImplicitAccountCreationAddressState = ( isBasicOutputsLoading, isNftOutputsLoading, isDelegationOutputsLoading, - }); + }; + + const addressOutputs = [...(addressBasicOutputs ?? []), ...(addressNftOutputs ?? [])].map(({ output }) => output); + if (protocolInfo?.parameters.storageScoreParameters) { + const storageDeposit = TransactionsHelper.computeStorageDeposit( + [...addressOutputs], + protocolInfo?.parameters.storageScoreParameters, + ); + updatedState = { + ...updatedState, + storageDeposit, + }; + } + + setState(updatedState); }, [ totalBaseTokenBalance, availableBaseTokenBalance, diff --git a/client/src/helpers/nova/hooks/useNftAddressState.ts b/client/src/helpers/nova/hooks/useNftAddressState.ts index b964107c6..bf54ac67b 100644 --- a/client/src/helpers/nova/hooks/useNftAddressState.ts +++ b/client/src/helpers/nova/hooks/useNftAddressState.ts @@ -9,6 +9,7 @@ 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 { TransactionsHelper } from "../transactionsHelper"; import { useAddressDelegationOutputs } from "./useAddressDelegationOutputs"; import { IManaBalance } from "~/models/api/nova/address/IAddressBalanceResponse"; import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails"; @@ -16,6 +17,7 @@ import { IDelegationWithDetails } from "~/models/api/nova/IDelegationWithDetails export interface INftAddressState { addressDetails: IAddressDetails | null; nftOutput: NftOutput | null; + storageDeposit: number | null; totalBaseTokenBalance: number | null; availableBaseTokenBalance: number | null; totalManaBalance: IManaBalance | null; @@ -36,7 +38,7 @@ export interface INftAddressState { const initialState = { addressDetails: null, nftOutput: null, - isNftDetailsLoading: true, + storageDeposit: null, totalBaseTokenBalance: null, availableBaseTokenBalance: null, totalManaBalance: null, @@ -45,6 +47,7 @@ const initialState = { addressBasicOutputs: null, addressNftOutputs: null, addressDelegationOutputs: null, + isNftDetailsLoading: true, isBasicOutputsLoading: false, isNftOutputsLoading: false, isDelegationOutputsLoading: false, @@ -63,7 +66,7 @@ interface IAddressPageLocationProps { export const useNftAddressState = (address: NftAddress): [INftAddressState, React.Dispatch>] => { const location = useLocation(); const { network } = useParams(); - const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo); + const { bech32Hrp, protocolInfo } = useNetworkInfoNova((s) => s.networkInfo); const [state, setState] = useReducer>>( (currentState, newState) => ({ ...currentState, ...newState }), initialState, @@ -104,7 +107,7 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac }, [address.nftId]); useEffect(() => { - setState({ + let updatedState: Partial = { nftOutput, totalBaseTokenBalance, availableBaseTokenBalance, @@ -118,7 +121,23 @@ export const useNftAddressState = (address: NftAddress): [INftAddressState, Reac isBasicOutputsLoading, isNftOutputsLoading, isDelegationOutputsLoading, - }); + }; + + if (nftOutput) { + const addressOutputs = [...(addressBasicOutputs ?? []), ...(addressNftOutputs ?? [])].map(({ output }) => output); + if (protocolInfo?.parameters.storageScoreParameters) { + const storageDeposit = TransactionsHelper.computeStorageDeposit( + [...addressOutputs, nftOutput], + protocolInfo?.parameters.storageScoreParameters, + ); + updatedState = { + ...updatedState, + storageDeposit, + }; + } + } + + setState(updatedState); }, [ nftOutput, totalBaseTokenBalance, diff --git a/client/src/helpers/nova/transactionsHelper.ts b/client/src/helpers/nova/transactionsHelper.ts index 0914b8ff2..d068021e0 100644 --- a/client/src/helpers/nova/transactionsHelper.ts +++ b/client/src/helpers/nova/transactionsHelper.ts @@ -18,11 +18,13 @@ import { MetadataFeature, NftAddress, NftOutput, + Output, OutputType, PayloadType, SignatureUnlock, SignedTransactionPayload, StateControllerAddressUnlockCondition, + StorageScoreParameters, UnlockCondition, UnlockConditionType, UnlockType, @@ -300,4 +302,26 @@ export class TransactionsHelper { return address; } + + /** + * Computes the storage deposit for a given set of outputs based on the rent structure. + * @param outputs The outputs to compute the storage deposit for. + * @param rentStructure The rent structure parameters. + * @returns The computed storage deposit. + */ + public static computeStorageDeposit(outputs: Output[], rentStructure: StorageScoreParameters): number { + const outputsWithoutSdruc = outputs.filter((output) => { + const hasStorageDepositUnlockCondition = (output as CommonOutput).unlockConditions.some( + (uc) => uc.type === UnlockConditionType.StorageDepositReturn, + ); + + return !hasStorageDepositUnlockCondition; + }); + + const rentBalance = outputsWithoutSdruc.reduce( + (acc, output) => acc + Number(Utils.computeMinimumOutputAmount(output, rentStructure)), + 0, + ); + return rentBalance; + } } diff --git a/client/src/services/nova/novaApiClient.ts b/client/src/services/nova/novaApiClient.ts index c585fbb5c..2f1c2775b 100644 --- a/client/src/services/nova/novaApiClient.ts +++ b/client/src/services/nova/novaApiClient.ts @@ -183,6 +183,15 @@ export class NovaApiClient extends ApiClient { return this.callApi(`nova/address/outputs/nft/${request.network}/${request.address}`, "get"); } + /** + * Get the anchor outputs details of an address. + * @param request The Address Anchor outputs request. + * @returns The Address outputs response + */ + public async anchorOutputsDetails(request: IAddressDetailsRequest): Promise { + return this.callApi(`nova/address/outputs/anchor/${request.network}/${request.address}`, "get"); + } + /** * Get the delegation outputs details of an address. * @param request The Address Delegation outputs request.