Skip to content

Commit

Permalink
Feat: Add nova address transaction history (#1158)
Browse files Browse the repository at this point in the history
* feat: Add transaction history api (nova)

* feat: Add TransactionHistoryView to AddressPageTabbedSection (and support in state hooks)

* feat: Fix param passing and chronicle ledger updates endpoint

* feat: Fix transaction date computation

* fix: Fix Address balance rendering

* feat: Disable Transactions tab if no transactions

* feat: Show balance from the address output even if no chronicle data
  • Loading branch information
msarcev authored Feb 27, 2024
1 parent d8a6acd commit 9414cbb
Show file tree
Hide file tree
Showing 30 changed files with 981 additions and 15 deletions.
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

0 comments on commit 9414cbb

Please sign in to comment.