Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Add storage deposit to balance #1208

Merged
merged 8 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions api/src/routes/nova/address/outputs/anchor/get.ts
Original file line number Diff line number Diff line change
@@ -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<IAddressDetailsResponse> {
const networkService = ServiceFactory.get<NetworkService>("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<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.anchorOutputDetailsByAddress(request.address);
}
31 changes: 29 additions & 2 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<IAddressDetailsResponse> {
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.
Expand Down Expand Up @@ -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<IDelegationDetailsResponse> {
let cursor: string | undefined;
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/components/nova/address/AccountAddressView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const AccountAddressView: React.FC<AccountAddressViewProps> = ({ accountAddress
const [state, setState] = useAccountAddressState(accountAddress);
const {
addressDetails,
storageDeposit,
totalBaseTokenBalance,
availableBaseTokenBalance,
totalManaBalance,
Expand Down Expand Up @@ -51,6 +52,7 @@ const AccountAddressView: React.FC<AccountAddressViewProps> = ({ accountAddress
totalManaBalance={totalManaBalance}
availableManaBalance={availableManaBalance}
blockIssuanceCredits={congestion?.blockIssuanceCredits}
storageDeposit={storageDeposit}
manaRewards={manaRewards}
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion client/src/app/components/nova/address/AnchorAddressView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const AnchorAddressView: React.FC<AnchorAddressViewProps> = ({ anchorAddress })
const [state, setState] = useAnchorAddressState(anchorAddress);
const {
addressDetails,
storageDeposit,
totalBaseTokenBalance,
availableBaseTokenBalance,
totalManaBalance,
Expand Down Expand Up @@ -49,7 +50,7 @@ const AnchorAddressView: React.FC<AnchorAddressViewProps> = ({ anchorAddress })
availableBaseTokenBalance={availableBaseTokenBalance}
totalManaBalance={totalManaBalance}
availableManaBalance={availableManaBalance}
storageDeposit={null}
storageDeposit={storageDeposit}
manaRewards={manaRewards}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const Ed25519AddressView: React.FC<Ed25519AddressViewProps> = ({ ed25519Address
const [state, setState] = useEd25519AddressState(ed25519Address);
const {
addressDetails,
storageDeposit,
totalBaseTokenBalance,
availableBaseTokenBalance,
totalManaBalance,
Expand Down Expand Up @@ -49,7 +50,7 @@ const Ed25519AddressView: React.FC<Ed25519AddressViewProps> = ({ ed25519Address
availableBaseTokenBalance={availableBaseTokenBalance}
totalManaBalance={totalManaBalance}
availableManaBalance={availableManaBalance}
storageDeposit={null}
storageDeposit={storageDeposit}
manaRewards={manaRewards}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface ImplicitAccountCreationAddressViewProps {

const ImplicitAccountCreationAddressView: React.FC<ImplicitAccountCreationAddressViewProps> = ({ implicitAccountCreationAddress }) => {
const [state, setState] = useImplicitAccountCreationAddressState(implicitAccountCreationAddress);
const { addressDetails, totalBaseTokenBalance, availableBaseTokenBalance, isAssociatedOutputsLoading } = state;
const { addressDetails, storageDeposit, totalBaseTokenBalance, availableBaseTokenBalance, isAssociatedOutputsLoading } = state;
const isPageLoading = isAssociatedOutputsLoading;

return (
Expand Down Expand Up @@ -40,7 +40,7 @@ const ImplicitAccountCreationAddressView: React.FC<ImplicitAccountCreationAddres
availableBaseTokenBalance={availableBaseTokenBalance}
totalManaBalance={null}
availableManaBalance={null}
storageDeposit={null}
storageDeposit={storageDeposit}
/>
</div>
</div>
Expand Down
3 changes: 2 additions & 1 deletion client/src/app/components/nova/address/NftAddressView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const NftAddressView: React.FC<NftAddressViewProps> = ({ nftAddress }) => {
const [state, setState] = useNftAddressState(nftAddress);
const {
addressDetails,
storageDeposit,
totalBaseTokenBalance,
availableBaseTokenBalance,
totalManaBalance,
Expand Down Expand Up @@ -49,7 +50,7 @@ const NftAddressView: React.FC<NftAddressViewProps> = ({ nftAddress }) => {
availableBaseTokenBalance={availableBaseTokenBalance}
totalManaBalance={totalManaBalance}
availableManaBalance={availableManaBalance}
storageDeposit={null}
storageDeposit={storageDeposit}
manaRewards={manaRewards}
/>
</div>
Expand Down
22 changes: 20 additions & 2 deletions client/src/helpers/nova/hooks/useAccountAddressState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -57,6 +59,7 @@ export interface IAccountAddressState {
const initialState = {
addressDetails: null,
accountOutput: null,
storageDeposit: null,
totalBaseTokenBalance: null,
availableBaseTokenBalance: null,
totalManaBalance: null,
Expand Down Expand Up @@ -92,7 +95,7 @@ interface IAddressPageLocationProps {
export const useAccountAddressState = (address: AccountAddress): [IAccountAddressState, React.Dispatch<Partial<IAccountAddressState>>] => {
const location = useLocation();
const { network } = useParams<AddressRouteProps>();
const { bech32Hrp } = useNetworkInfoNova((s) => s.networkInfo);
const { bech32Hrp, protocolInfo } = useNetworkInfoNova((s) => s.networkInfo);
const [state, setState] = useReducer<Reducer<IAccountAddressState, Partial<IAccountAddressState>>>(
(currentState, newState) => ({ ...currentState, ...newState }),
initialState,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -197,6 +214,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres
manaRewards,
addressBasicOutputs,
addressNftOutputs,
accountFoundryOutputs,
addressDelegationOutputs,
congestion,
validatorDetails,
Expand Down
38 changes: 22 additions & 16 deletions client/src/helpers/nova/hooks/useAccountControlledFoundries.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<NovaApiClient>(`api-client-${NOVA}`));
const [accountFoundries, setAccountFoundries] = useState<string[] | null>(null);
const [accountFoundryOutputs, setAccountFoundryOutputs] = useState<OutputResponse[] | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);

useEffect(() => {
setIsLoading(true);
if (accountAddress) {
const foundries: string[] = [];
const outputResponses: OutputResponse[] = [];
// eslint-disable-next-line no-void
void (async () => {
apiClient
Expand All @@ -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);
}
}
})
Expand All @@ -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];
}
43 changes: 43 additions & 0 deletions client/src/helpers/nova/hooks/useAddressAnchorOutputs.ts
Original file line number Diff line number Diff line change
@@ -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<NovaApiClient>(`api-client-${NOVA}`));
const [outputs, setOutputs] = useState<OutputResponse[] | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(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];
}
Loading
Loading