Skip to content

Commit

Permalink
Feat: Add Block issuance tab to Account Address (#1142)
Browse files Browse the repository at this point in the history
* feat: add output tab to address page

* fix: add components to display native tokens

* Remove unused code in AddressPageTabbedSections component

* remove unused interfaces

* Update address states and Add basic outputs API endpoint

* Add eslint-disable for unsafe return in novaApiService.ts

* Add Foundries tab to Accound address page

* Add Foundries tab to Accound address page (#1134)

* feat: add block issuance tab to account page

* fix: validation check in foundries endpoint

* fix: component imports

* fix: add is congestion loading

* Update paths in tsconfig.json

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
  • Loading branch information
brancoder and begonaalvarezd authored Feb 22, 2024
1 parent ea34f5d commit 42477e3
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 7 deletions.
11 changes: 11 additions & 0 deletions api/src/models/api/nova/ICongestionRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface ICongestionRequest {
/**
* The network to search on.
*/
network: string;

/**
* The account id to get the congestion for.
*/
accountId: string;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/ICongestionResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { CongestionResponse } from "@iota/sdk-nova";
import { IResponse } from "./IResponse";

export interface ICongestionResponse extends IResponse {
/**
* The Account Congestion.
*/
congestion?: CongestionResponse;
}
6 changes: 6 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,12 @@ export const routes: IRoute[] = [
folder: "nova/account/foundries",
func: "get",
},
{
path: "/nova/account/congestion/:network/:accountId",
method: "get",
folder: "nova/account/congestion",
func: "get",
},
{ path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", func: "get" },
{ path: "/nova/block/metadata/:network/:blockId", method: "get", folder: "nova/block/metadata", func: "get" },
];
30 changes: 30 additions & 0 deletions api/src/routes/nova/account/congestion/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ServiceFactory } from "../../../../factories/serviceFactory";
import { ICongestionRequest } from "../../../../models/api/nova/ICongestionRequest";
import { ICongestionResponse } from "../../../../models/api/nova/ICongestionResponse";
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";

/**
* Get Congestion for Account address
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: ICongestionRequest): Promise<ICongestionResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.string(request.accountId, "accountId");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const novaApiService = ServiceFactory.get<NovaApiService>(`api-service-${networkConfig.network}`);
return novaApiService.getAccountCongestion(request.accountId);
}
20 changes: 20 additions & 0 deletions api/src/services/nova/novaApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IAddressDetailsResponse } from "../../models/api/nova/IAddressDetailsRe
import { IAnchorDetailsResponse } from "../../models/api/nova/IAnchorDetailsResponse";
import { IBlockDetailsResponse } from "../../models/api/nova/IBlockDetailsResponse";
import { IBlockResponse } from "../../models/api/nova/IBlockResponse";
import { ICongestionResponse } from "../../models/api/nova/ICongestionResponse";
import { INftDetailsResponse } from "../../models/api/nova/INftDetailsResponse";
import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse";
import { IRewardsResponse } from "../../models/api/nova/IRewardsResponse";
Expand Down Expand Up @@ -250,6 +251,25 @@ export class NovaApiService {
};
}

/**
* Get Congestion for Account
* @param accountId The account address to get the congestion for.
* @returns The Congestion.
*/
public async getAccountCongestion(accountId: string): Promise<ICongestionResponse | undefined> {
try {
const response = await this.client.getAccountCongestion(accountId);

if (response) {
return {
congestion: response,
};
}
} catch {
return { message: "Account congestion not found" };
}
}

/**
* Get the output mana rewards.
* @param outputId The outputId to get the rewards for.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import associatedOuputsMessage from "~assets/modals/stardust/address/associated-outputs.json";
import foundriesMessage from "~assets/modals/stardust/alias/foundries.json";
import bicMessage from "~assets/modals/nova/account/bic.json";
import TabbedSection from "../../../hoc/TabbedSection";
import AssociatedOutputs from "./association/AssociatedOutputs";
import nativeTokensMessage from "~assets/modals/stardust/address/assets-in-wallet.json";
Expand All @@ -12,13 +13,15 @@ import AssetsTable from "./native-tokens/AssetsTable";
import { IImplicitAccountCreationAddressState } from "~/helpers/nova/hooks/useImplicitAccountCreationAddressState";
import { AddressType } from "@iota/sdk-wasm-nova/web";
import AccountFoundriesSection from "./account/AccountFoundriesSection";
import AccountBlockIssuanceSection from "./account/AccountBlockIssuanceSection";

enum DEFAULT_TABS {
AssocOutputs = "Outputs",
NativeTokens = "Native Tokens",
}

enum ACCOUNT_TABS {
BlockIssuance = "Block Issuance",
Foundries = "Foundries",
}

Expand All @@ -37,13 +40,24 @@ const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: num
},
});

const buildAccountAddressTabsOptions = (foundriesCount: number, isAccountFoundriesLoading: boolean) => ({
const buildAccountAddressTabsOptions = (
isBlockIssuer: boolean,
isCongestionLoading: boolean,
foundriesCount: number,
isAccountFoundriesLoading: boolean,
) => ({
[ACCOUNT_TABS.Foundries]: {
disabled: foundriesCount === 0,
hidden: foundriesCount === 0,
isLoading: isAccountFoundriesLoading,
infoContent: foundriesMessage,
},
[ACCOUNT_TABS.BlockIssuance]: {
disabled: !isBlockIssuer,
hidden: !isBlockIssuer,
isLoading: isCongestionLoading,
infoContent: bicMessage,
},
});

interface IAddressPageTabbedSectionsProps {
Expand Down Expand Up @@ -78,6 +92,11 @@ export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps
const accountAddressSections =
addressDetails.type === AddressType.Account
? [
<AccountBlockIssuanceSection
key={`account-block-issuance-${addressDetails.bech32}`}
blockIssuerFeature={(addressState as IAccountAddressState).blockIssuerFeature}
congestion={(addressState as IAccountAddressState).congestion}
/>,
<AccountFoundriesSection
key={`account-foundry-${addressDetails.bech32}`}
foundries={(addressState as IAccountAddressState).foundries}
Expand All @@ -92,12 +111,15 @@ export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps

switch (addressDetails.type) {
case AddressType.Account: {
const accountAddressState = addressState as IAccountAddressState;
tabEnums = { ...DEFAULT_TABS, ...ACCOUNT_TABS };
tabOptions = {
...defaultTabsOptions,
...buildAccountAddressTabsOptions(
(addressState as IAccountAddressState).accountOutput?.foundryCounter ?? 0,
(addressState as IAccountAddressState).isFoundriesLoading,
accountAddressState.blockIssuerFeature !== null,
accountAddressState.isCongestionLoading,
accountAddressState.accountOutput?.foundryCounter ?? 0,
accountAddressState.isFoundriesLoading,
),
};
tabbedSections = [...defaultSections, ...(accountAddressSections ?? [])];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@import "../../../../../../scss/mixins.scss";

.block-issuance--card {
border: none !important;
margin-bottom: 48px;

.field {
margin-bottom: 8px;

.card--label {
@include font-size(14px, 21px);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React from "react";
import { BlockIssuerFeature, CongestionResponse, Ed25519PublicKeyHashBlockIssuerKey } from "@iota/sdk-wasm-nova/web";
import TruncatedId from "~/app/components/stardust/TruncatedId";
import "./AccountBlockIssuanceSection.scss";

interface AccountBlockIssuanceSectionProps {
readonly blockIssuerFeature: BlockIssuerFeature | null;
readonly congestion: CongestionResponse | null;
}

const AccountBlockIssuanceSection: React.FC<AccountBlockIssuanceSectionProps> = ({ blockIssuerFeature, congestion }) => {
return (
<div className="section transaction--section">
<div className="card block-issuance--card">
<div className="field">
<div className="card--label margin-b-t">Current Slot</div>
<div className="card--value">{congestion?.slot}</div>
</div>
<div className="field">
<div className="card--label margin-b-t">Block Issuance Credit</div>
<div className="card--value">{congestion?.blockIssuanceCredits.toString()}</div>
</div>
<div className="field">
<div className="card--label margin-b-t">Referenced Mana Cost</div>
<div className="card--value">{congestion?.referenceManaCost.toString()}</div>
</div>
<div className="field">
<div className="card--label margin-b-t">Expiry Slot</div>
<div className="card--value">{blockIssuerFeature?.expirySlot}</div>
</div>
<div className="field">
{blockIssuerFeature?.blockIssuerKeys.map((key) => (
<React.Fragment key={(key as Ed25519PublicKeyHashBlockIssuerKey).pubKeyHash}>
<span className="card--label margin-b-t">Public Key:</span>
<div className="card--value public-key">
<TruncatedId id={(key as Ed25519PublicKeyHashBlockIssuerKey).pubKeyHash} showCopyButton />
</div>
</React.Fragment>
))}
</div>
</div>
</div>
);
};

export default AccountBlockIssuanceSection;
11 changes: 11 additions & 0 deletions client/src/assets/modals/nova/account/bic.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"title": "Block Issuance Credit",
"description": "<p>(BIC) is the form of Mana used as an anti-spam mechanism to the block issuance process.</p>",
"links": [
{
"label": "Read more",
"href": "https://wiki.iota.org/learn/protocols/iota2.0/core-concepts/mana/#block-issuance-credits-bic",
"isExternal": true
}
]
}
48 changes: 44 additions & 4 deletions client/src/helpers/nova/hooks/useAccountAddressState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { Reducer, useEffect, useReducer } from "react";
import { AccountAddress, AccountOutput, OutputResponse } from "@iota/sdk-wasm-nova/web";
import {
AccountAddress,
AccountOutput,
BlockIssuerFeature,
CongestionResponse,
FeatureType,
OutputResponse,
} from "@iota/sdk-wasm-nova/web";
import { IAddressDetails } from "~/models/api/nova/IAddressDetails";
import { useAccountDetails } from "./useAccountDetails";
import { useLocation, useParams } from "react-router-dom";
Expand All @@ -9,31 +16,38 @@ import { AddressHelper } from "~/helpers/nova/addressHelper";
import { useAddressBalance } from "./useAddressBalance";
import { useAddressBasicOutputs } from "~/helpers/nova/hooks/useAddressBasicOutputs";
import { useAccountControlledFoundries } from "./useAccountControlledFoundries";
import { useAccountCongestion } from "./useAccountCongestion";

export interface IAccountAddressState {
addressDetails: IAddressDetails | null;
accountOutput: AccountOutput | null;
totalBalance: number | null;
availableBalance: number | null;
blockIssuerFeature: BlockIssuerFeature | null;
addressBasicOutputs: OutputResponse[] | null;
foundries: string[] | null;
congestion: CongestionResponse | null;
isAccountDetailsLoading: boolean;
isAssociatedOutputsLoading: boolean;
isBasicOutputsLoading: boolean;
isFoundriesLoading: boolean;
isCongestionLoading: boolean;
}

const initialState = {
addressDetails: null,
accountOutput: null,
totalBalance: null,
availableBalance: null,
blockIssuerFeature: null,
addressBasicOutputs: null,
foundries: null,
congestion: null,
isAccountDetailsLoading: true,
isAssociatedOutputsLoading: false,
isBasicOutputsLoading: false,
isFoundriesLoading: false,
isCongestionLoading: false,
};

/**
Expand All @@ -56,6 +70,7 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres
const { totalBalance, availableBalance } = useAddressBalance(network, state.addressDetails, accountOutput);
const [addressBasicOutputs, isBasicOutputsLoading] = useAddressBasicOutputs(network, state.addressDetails?.bech32 ?? null);
const [foundries, isFoundriesLoading] = useAccountControlledFoundries(network, state.addressDetails);
const { congestion, isLoading: isCongestionLoading } = useAccountCongestion(network, state.addressDetails?.hex ?? null);

useEffect(() => {
const locationState = location.state as IAddressPageLocationProps;
Expand All @@ -70,17 +85,42 @@ export const useAccountAddressState = (address: AccountAddress): [IAccountAddres
}, []);

useEffect(() => {
setState({
let updatedState: Partial<IAccountAddressState> = {
accountOutput,
isAccountDetailsLoading,
totalBalance,
availableBalance,
foundries,
congestion,
addressBasicOutputs,
isBasicOutputsLoading,
isFoundriesLoading,
});
}, [accountOutput, totalBalance, availableBalance, addressBasicOutputs, isAccountDetailsLoading, isBasicOutputsLoading]);
isCongestionLoading,
};

if (accountOutput && !state.blockIssuerFeature) {
const blockIssuerFeature = accountOutput?.features?.find(
(feature) => feature.type === FeatureType.BlockIssuer,
) as BlockIssuerFeature;
if (blockIssuerFeature) {
updatedState = {
...updatedState,
blockIssuerFeature,
};
}
}

setState(updatedState);
}, [
accountOutput,
totalBalance,
availableBalance,
addressBasicOutputs,
congestion,
isAccountDetailsLoading,
isBasicOutputsLoading,
isCongestionLoading,
]);

return [state, setState];
};
Loading

0 comments on commit 42477e3

Please sign in to comment.