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 nova address transaction history #1158

Merged
merged 8 commits into from
Feb 27, 2024
7 changes: 4 additions & 3 deletions api/src/initServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import { LegacyStatsService } from "./services/legacy/legacyStatsService";
import { ZmqService } from "./services/legacy/zmqService";
import { LocalStorageService } from "./services/localStorageService";
import { NetworkService } from "./services/networkService";
import { ChronicleService as ChronicleServiceNova } from "./services/nova/chronicleService";
import { NovaFeed } from "./services/nova/feed/novaFeed";
import { NodeInfoService as NodeInfoServiceNova } from "./services/nova/nodeInfoService";
import { NovaApiService } from "./services/nova/novaApiService";
import { NovaStatsService } from "./services/nova/stats/novaStatsService";
import { ChronicleService } from "./services/stardust/chronicleService";
import { ChronicleService as ChronicleServiceStardust } from "./services/stardust/chronicleService";
import { StardustFeed } from "./services/stardust/feed/stardustFeed";
import { InfluxDBService } from "./services/stardust/influx/influxDbService";
import { NodeInfoService as NodeInfoServiceStardust } from "./services/stardust/nodeInfoService";
Expand Down Expand Up @@ -170,7 +171,7 @@ function initStardustServices(networkConfig: INetwork): void {
// Related: https://github.com/iotaledger/inx-chronicle/issues/1302
stardustClientParams.ignoreNodeHealth = true;

const chronicleService = new ChronicleService(networkConfig);
const chronicleService = new ChronicleServiceStardust(networkConfig);
ServiceFactory.register(`chronicle-${networkConfig.network}`, () => chronicleService);
}

Expand Down Expand Up @@ -221,7 +222,7 @@ function initNovaServices(networkConfig: INetwork): void {
};
novaClientParams.primaryNodes.push(chronicleNode);

const chronicleService = new ChronicleService(networkConfig);
const chronicleService = new ChronicleServiceNova(networkConfig);
ServiceFactory.register(`chronicle-${networkConfig.network}`, () => chronicleService);
}

Expand Down
14 changes: 12 additions & 2 deletions api/src/models/api/nova/chronicle/IAddressBalanceResponse.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import { IResponse } from "../../IResponse";

interface IManaBalance {
stored: number;
potential: number;
}

interface IBalance {
amount: number;
mana: IManaBalance;
}

export interface IAddressBalanceResponse extends IResponse {
/**
* The total balance (including Expiration, Timelock and StorageDepositReturn outputs)
*/
totalBalance?: number;
totalBalance?: IBalance;

/**
* The balance of all spendable outputs by the address at this time.
*/
availableBalance?: number;
availableBalance?: IBalance;

/**
* The ledger index at which this balance data was valid.
Expand Down
34 changes: 34 additions & 0 deletions api/src/models/api/nova/chronicle/ITransactionHistoryRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* The request for Transaction History on nova.
*/
export interface ITransactionHistoryRequest {
/**
* The network in context.
*/
network: string;

/**
* The address to get the history for.
*/
address: string;

/**
* The page size of the request (default is 100).
*/
pageSize?: number;

/**
* The sort by date to use.
*/
sort?: string;

/**
* The lower bound slot index to use.
*/
startSlotIndex?: number;

/**
* The cursor state for the request.
*/
cursor?: string;
}
41 changes: 41 additions & 0 deletions api/src/models/api/nova/chronicle/ITransactionHistoryResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { IResponse } from "../../IResponse";

/**
* A transaction history item.
*/
export interface ITransactionHistoryItem {
/**
* The slot index this item is included in.
*/
slotIndex: number;

/**
* The outputId.
*/
outputId: string;

/**
* Is the output spent.
*/
isSpent: boolean;
}

/*
* The transaction history response.
*/
export interface ITransactionHistoryResponse extends IResponse {
/**
* Address the history is for.
*/
address?: string;

/**
* The history items.
*/
items?: ITransactionHistoryItem[];

/**
* The cursor for next request.
*/
cursor?: string;
}
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/transactionhistory/:network/:address",
method: "get",
folder: "nova/transactionhistory",
func: "get",
},
{
path: "/nova/transaction/:network/:transactionId",
method: "get",
Expand Down
34 changes: 34 additions & 0 deletions api/src/routes/nova/transactionhistory/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ServiceFactory } from "../../../factories/serviceFactory";
import { ITransactionHistoryRequest } from "../../../models/api/nova/chronicle/ITransactionHistoryRequest";
import { ITransactionHistoryResponse } from "../../../models/api/nova/chronicle/ITransactionHistoryResponse";
import { IConfiguration } from "../../../models/configuration/IConfiguration";
import { NOVA } from "../../../models/db/protocolVersion";
import { NetworkService } from "../../../services/networkService";
import { ChronicleService } from "../../../services/nova/chronicleService";
import { ValidationHelper } from "../../../utils/validationHelper";

/**
* Fetch the transaction history from chronicle nova.
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(config: IConfiguration, request: ITransactionHistoryRequest): Promise<ITransactionHistoryResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");

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

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

if (!networkConfig.permaNodeEndpoint) {
return {};
}

const chronicleService = ServiceFactory.get<ChronicleService>(`chronicle-${networkConfig.network}`);

return chronicleService.transactionHistory(request);
}
28 changes: 28 additions & 0 deletions api/src/services/nova/chronicleService.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import logger from "../../logger";
import { IAddressBalanceResponse } from "../../models/api/nova/chronicle/IAddressBalanceResponse";
import { ITransactionHistoryRequest } from "../../models/api/nova/chronicle/ITransactionHistoryRequest";
import { ITransactionHistoryResponse } from "../../models/api/nova/chronicle/ITransactionHistoryResponse";
import { INetwork } from "../../models/db/INetwork";
import { FetchHelper } from "../../utils/fetchHelper";

const CHRONICLE_ENDPOINTS = {
updatesByAddress: "/api/explorer/v3/ledger/updates/by-address/",
balance: "/api/explorer/v3/balance/",
};

Expand Down Expand Up @@ -40,4 +43,29 @@ export class ChronicleService {
logger.warn(`[ChronicleService (Nova)] Failed fetching address balance for ${address} on ${network}. Cause: ${error}`);
}
}

/**
* Get the transaction history of an address.
* @param request The ITransactionHistoryRequest.
* @returns The history reponse.
*/
public async transactionHistory(request: ITransactionHistoryRequest): Promise<ITransactionHistoryResponse | undefined> {
try {
const params = {
pageSize: request.pageSize,
sort: request.sort,
startSlotIndex: request.startSlotIndex,
cursor: request.cursor,
};

return await FetchHelper.json<never, ITransactionHistoryResponse>(
this.chronicleEndpoint,
`${CHRONICLE_ENDPOINTS.updatesByAddress}${request.address}${params ? `${FetchHelper.urlParams(params)}` : ""}`,
"get",
);
} catch (error) {
const network = this.networkConfig.network;
logger.warn(`[ChronicleService] Failed fetching tx history for ${request.address} on ${network}. Cause: ${error}`);
}
}
}
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 @@ -49,6 +49,8 @@ const AccountAddressView: React.FC<AccountAddressViewProps> = ({ accountAddress
key={addressDetails.bech32}
addressState={state}
setAssociatedOutputsLoading={(val) => setState({ isAssociatedOutputsLoading: val })}
setTransactionHistoryLoading={(isLoading) => setState({ isAddressHistoryLoading: isLoading })}
setTransactionHistoryDisabled={(val) => setState({ isAddressHistoryDisabled: val })}
/>
</div>
)}
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/components/nova/address/AnchorAddressView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const AnchorAddressView: React.FC<AnchorAddressViewProps> = ({ anchorAddress })
key={addressDetails.bech32}
addressState={state}
setAssociatedOutputsLoading={(val) => setState({ isAssociatedOutputsLoading: val })}
setTransactionHistoryLoading={(isLoading) => setState({ isAddressHistoryLoading: isLoading })}
setTransactionHistoryDisabled={(val) => setState({ isAddressHistoryDisabled: val })}
/>
</div>
)}
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/components/nova/address/Ed25519AddressView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const Ed25519AddressView: React.FC<Ed25519AddressViewProps> = ({ ed25519Address
key={addressDetails.bech32}
addressState={state}
setAssociatedOutputsLoading={(val) => setState({ isAssociatedOutputsLoading: val })}
setTransactionHistoryLoading={(isLoading) => setState({ isAddressHistoryLoading: isLoading })}
setTransactionHistoryDisabled={(val) => setState({ isAddressHistoryDisabled: val })}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const ImplicitAccountCreationAddressView: React.FC<ImplicitAccountCreationAddres
key={addressDetails.bech32}
addressState={state}
setAssociatedOutputsLoading={(val) => setState({ isAssociatedOutputsLoading: val })}
setTransactionHistoryLoading={(isLoading) => setState({ isAddressHistoryLoading: isLoading })}
setTransactionHistoryDisabled={(val) => setState({ isAddressHistoryDisabled: val })}
/>
</div>
)}
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/components/nova/address/NftAddressView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const NftAddressView: React.FC<NftAddressViewProps> = ({ nftAddress }) => {
key={addressDetails.bech32}
addressState={state}
setAssociatedOutputsLoading={(val) => setState({ isAssociatedOutputsLoading: val })}
setTransactionHistoryLoading={(isLoading) => setState({ isAddressHistoryLoading: isLoading })}
setTransactionHistoryDisabled={(val) => setState({ isAddressHistoryDisabled: val })}
/>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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";
import transactionHistoryMessage from "~assets/modals/stardust/address/transaction-history.json";
import { IAccountAddressState } from "~/helpers/nova/hooks/useAccountAddressState";
import { INftAddressState } from "~/helpers/nova/hooks/useNftAddressState";
import { IAnchorAddressState } from "~/helpers/nova/hooks/useAnchorAddressState";
Expand All @@ -14,10 +15,13 @@ 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 TransactionHistory from "../../history/TransactionHistoryView";
import { useNetworkInfoNova } from "~/helpers/nova/networkInfo";
import AccountBlockIssuanceSection from "./account/AccountBlockIssuanceSection";
import AnchorStateSection from "./anchor/AnchorStateSection";

enum DEFAULT_TABS {
Transactions = "Transactions",
AssocOutputs = "Outputs",
NativeTokens = "Native Tokens",
}
Expand All @@ -31,7 +35,18 @@ enum ANCHOR_TABS {
State = "State",
}

const buildDefaultTabsOptions = (tokensCount: number, associatedOutputCount: number) => ({
const buildDefaultTabsOptions = (
tokensCount: number,
associatedOutputCount: number,
isAddressHistoryLoading: boolean,
isAddressHistoryDisabled: boolean,
) => ({
[DEFAULT_TABS.Transactions]: {
disabled: isAddressHistoryDisabled,
hidden: isAddressHistoryDisabled,
isLoading: isAddressHistoryLoading,
infoContent: transactionHistoryMessage,
},
[DEFAULT_TABS.AssocOutputs]: {
disabled: associatedOutputCount === 0,
hidden: associatedOutputCount === 0,
Expand Down Expand Up @@ -83,18 +98,35 @@ interface IAddressPageTabbedSectionsProps {
| IAnchorAddressState
| IImplicitAccountCreationAddressState;
readonly setAssociatedOutputsLoading: (isLoading: boolean) => void;
readonly setTransactionHistoryLoading: (isLoading: boolean) => void;
readonly setTransactionHistoryDisabled: (isDisabled: boolean) => void;
}

export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps> = ({ addressState, setAssociatedOutputsLoading }) => {
export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps> = ({
addressState,
setAssociatedOutputsLoading,
setTransactionHistoryLoading,
setTransactionHistoryDisabled,
}) => {
const [outputCount, setOutputCount] = useState<number>(0);
const [tokensCount, setTokensCount] = useState<number>(0);
const networkInfo = useNetworkInfoNova((s) => s.networkInfo);

if (!addressState.addressDetails) {
return null;
}
const { addressDetails, addressBasicOutputs } = addressState;
const { addressDetails, addressBasicOutputs, isAddressHistoryLoading, isAddressHistoryDisabled } = addressState;
const { bech32: addressBech32 } = addressDetails;
const { name: network } = networkInfo;

const defaultSections = [
<TransactionHistory
key={`txs-history-${addressBech32}`}
network={network}
address={addressBech32}
setLoading={setTransactionHistoryLoading}
setDisabled={setTransactionHistoryDisabled}
/>,
<AssociatedOutputs
key={`assoc-outputs-${addressDetails.bech32}`}
addressDetails={addressDetails}
Expand Down Expand Up @@ -130,7 +162,7 @@ export const AddressPageTabbedSections: React.FC<IAddressPageTabbedSectionsProps
: null;

let tabEnums = DEFAULT_TABS;
const defaultTabsOptions = buildDefaultTabsOptions(tokensCount, outputCount);
const defaultTabsOptions = buildDefaultTabsOptions(tokensCount, outputCount, isAddressHistoryLoading, isAddressHistoryDisabled);
let tabOptions = defaultTabsOptions;
let tabbedSections = defaultSections;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export interface ITransactionHistoryEntryProps {
/**
* The transaction id.
*/
transactionId: string;

/**
* The formatted date of the transaction.
*/
dateFormatted: string;

/**
* Is the transaction spent.
*/
isSpent: boolean;

/**
* Are the amounts formatted.
*/
isFormattedAmounts: boolean;

/**
* The setter for formatted amounts toggle.
*/
setIsFormattedAmounts: React.Dispatch<React.SetStateAction<boolean>>;

/**
* The formatted transaction amount.
*/
balanceChangeFormatted: string;

/**
* The transaction link.
*/
transactionLink: string;
}
Loading
Loading