Skip to content

Commit

Permalink
Feat: Add storage deposit to balance (#1208)
Browse files Browse the repository at this point in the history
* add storage deposit for all addreses

* feat: add get address anchor outputs endpoint

* feat: add get address anchor outputs endpoint

* Add basicOutputDetailsByAddress method to NovaApiService

* revert: Revert changes to client/tsconfig.json

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
Co-authored-by: Mario <[email protected]>
  • Loading branch information
3 people authored Mar 12, 2024
1 parent f952389 commit 3be27be
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 38 deletions.
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

0 comments on commit 3be27be

Please sign in to comment.