Skip to content

Commit

Permalink
Feat: Add OutputPage support (#933)
Browse files Browse the repository at this point in the history
* feat: Add infra to support OutputPage (nova)

* feat: Add partial support for output "nested" view and unlock conditions [WiP]

* feat: Add stardust-like outputId render to OutputView

---------

Co-authored-by: Begoña Álvarez de la Cruz <[email protected]>
  • Loading branch information
msarcev and begonaalvarezd authored Dec 20, 2023
1 parent 67c4360 commit 98e6b13
Show file tree
Hide file tree
Showing 17 changed files with 889 additions and 3 deletions.
9 changes: 9 additions & 0 deletions api/src/models/api/nova/IOutputDetailsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { OutputResponse } from "@iota/sdk-nova";
import { IResponse } from "./IResponse";

export interface IOutputDetailsResponse extends IResponse {
/**
* The output data.
*/
output?: OutputResponse;
}
11 changes: 11 additions & 0 deletions api/src/models/api/nova/IResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface IResponse {
/**
* An error for the response.
*/
error?: string;

/**
* A message for the response.
*/
message?: string;
}
4 changes: 3 additions & 1 deletion api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,7 @@ export const routes: IRoute[] = [
{
path: "/stardust/token/distribution/:network", method: "get",
folder: "stardust/address/distribution", func: "get"
}
},
// Nova
{ path: "/nova/output/:network/:outputId", method: "get", folder: "nova/output", func: "get" }
];
32 changes: 32 additions & 0 deletions api/src/routes/nova/output/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ServiceFactory } from "../../../factories/serviceFactory";
import { IOutputDetailsRequest } from "../../../models/api/chrysalis/IOutputDetailsRequest";
import { IOutputDetailsResponse } from "../../../models/api/nova/IOutputDetailsResponse";
import { IConfiguration } from "../../../models/configuration/IConfiguration";
import { NOVA } from "../../../models/db/protocolVersion";
import { NetworkService } from "../../../services/networkService";
import { NovaApi } from "../../../services/nova/novaApi";
import { ValidationHelper } from "../../../utils/validationHelper";

/**
* Find the object from the network.
* @param config The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(
config: IConfiguration,
request: IOutputDetailsRequest
): Promise<IOutputDetailsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");
ValidationHelper.string(request.outputId, "outputId");

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

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

return NovaApi.outputDetails(networkConfig, request.outputId);
}
67 changes: 67 additions & 0 deletions api/src/services/nova/novaApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
__ClientMethods__, OutputResponse, Client
} from "@iota/sdk-nova";
import { ServiceFactory } from "../../factories/serviceFactory";
import { IOutputDetailsResponse } from "../../models/api/nova/IOutputDetailsResponse";
import { INetwork } from "../../models/db/INetwork";

type NameType<T> = T extends { name: infer U } ? U : never;
type ExtractedMethodNames = NameType<__ClientMethods__>;

/**
* Class to interact with the nova API.
*/
export class NovaApi {
/**
* Get the output details.
* @param network The network to find the items on.
* @param outputId The output id to get the details.
* @returns The item details.
*/
public static async outputDetails(network: INetwork, outputId: string): Promise<IOutputDetailsResponse> {
const outputResponse = await this.tryFetchNodeThenPermanode<string, OutputResponse>(
outputId,
"getOutput",
network
);

return outputResponse ?
{ output: outputResponse } :
{ message: "Output not found" };
}

/**
* Generic helper function to try fetching from node client.
* On failure (or not present), we try to fetch from permanode (if configured).
* @param args The argument(s) to pass to the fetch calls.
* @param methodName The function to call on the client.
* @param network The network config in context.
* @returns The results or null if call(s) failed.
*/
public static async tryFetchNodeThenPermanode<A, R>(
args: A,
methodName: ExtractedMethodNames,
network: INetwork
): Promise<R> | null {
const { permaNodeEndpoint, disableApiFallback } = network;
const isFallbackEnabled = !disableApiFallback;
const client = ServiceFactory.get<Client>(`client-${network.network}`);

try {
// try fetch from node
const result: Promise<R> = client[methodName](args);
return await result;
} catch { }

if (permaNodeEndpoint && isFallbackEnabled) {
const permanodeClient = ServiceFactory.get<Client>(`permanode-client-${network.network}`);
try {
// try fetch from permanode (chronicle)
const result: Promise<R> = permanodeClient[methodName](args);
return await result;
} catch { }
}

return null;
}
}
5 changes: 4 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,8 @@
"not dead",
"not ie <= 11",
"not op_mini all"
]
],
"prettier": {
"tabWidth": 4
}
}
40 changes: 40 additions & 0 deletions client/src/app/components/nova/AddressView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from "react";
import { Address, AddressType } from "@iota/sdk-wasm-nova/web";

interface AddressViewProps {
address: Address;
}

const AddressView: React.FC<AddressViewProps> = ({ address }) => {
return (
<div className="address-type">
<div className="card--label">
{getAddressTypeName(address.type)}
</div>
<div className="card--value">
{JSON.stringify(address)}
</div>
</div>
);
};

function getAddressTypeName(type: AddressType): string {
switch (type) {
case AddressType.Ed25519:
return "Ed25519";
case AddressType.Account:
return "Account";
case AddressType.Nft:
return "Nft";
case AddressType.Anchor:
return "Anchor";
case AddressType.ImplicitAccountCreation:
return "ImplicitAccountCreation";
case AddressType.Multi:
return "Multi";
case AddressType.Restricted:
return "Restricted";
}
}

export default AddressView;
84 changes: 84 additions & 0 deletions client/src/app/components/nova/OutputView.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
@import "../../../scss/media-queries";
@import "../../../scss/variables";

.card--content__output {
padding: 0 30px;
margin-bottom: 20px;

@include phone-down {
padding: 0 4px;
}

.card--value.card-header--wrapper {
width: 100%;
display: flex;
margin-bottom: 0px;
height: 32px;
align-items: center;

.output-header {
display: flex;
width: 100%;

.output-type--name {
white-space: nowrap;
}

.output-id--link {
display: flex;
margin-right: 2px;
white-space: nowrap;

a {
margin-right: 0px;
}

.highlight {
font-weight: 500;
color: $gray-6;
margin-left: 2px;
}

.copy-button {
margin-left: 2px;
}
}
}

.amount-size {
width: min-content;
text-align: end;
word-break: normal;
white-space: nowrap;
cursor: pointer;
margin-bottom: 0;
margin-right: 4px;

span {
word-break: keep-all;
}
}
}

.left-border {
border-left: 1px solid var(--border-color);
}
}

.card--content--dropdown {
margin-right: 8px;
cursor: pointer;

svg {
transition: transform 0.25s ease;

path {
fill: var(--card-color);
}
}

&.opened > svg {
transform: rotate(90deg);
}
}

111 changes: 111 additions & 0 deletions client/src/app/components/nova/OutputView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from "react";
import DropdownIcon from "~assets/dropdown-arrow.svg?react";
import classNames from "classnames";
import { Output, OutputType, CommonOutput } from "@iota/sdk-wasm-nova/web";
import UnlockConditionView from "./UnlockConditionView";
import CopyButton from "../CopyButton";
import { Link } from "react-router-dom";
import "./OutputView.scss";

interface OutputViewProps {
outputId: string;
output: Output;
showCopyAmount: boolean;
}

const OutputView: React.FC<OutputViewProps> = ({
outputId,
output,
showCopyAmount,
}) => {
const [isExpanded, setIsExpanded] = React.useState(false);
const [isFormattedBalance, setIsFormattedBalance] = React.useState(true);

console.log(output);

const outputIdTransactionPart = `${outputId.slice(0, 8)}....${outputId.slice(-8, -4)}`;
const outputIdIndexPart = outputId.slice(-4);

return (
<div className="card--content__output">
<div
onClick={() => setIsExpanded(!isExpanded)}
className="card--value card-header--wrapper"
>
<div
className={classNames("card--content--dropdown", {
opened: isExpanded,
})}
>
<DropdownIcon />
</div>
<div className="output-header">
<button type="button" className="output-type--name color">
{getOutputTypeName(output.type)}
</button>
<div className="output-id--link">
(
<Link
// TODO need the network context here
to={`/networkContext/output/${outputId}`}
className="margin-r-t"
>
<span>{outputIdTransactionPart}</span>
<span className="highlight">
{outputIdIndexPart}
</span>
</Link>
)
<CopyButton copy={String(outputId)} />
</div>
</div>
{showCopyAmount && (
<div className="card--value pointer amount-size row end">
<span
className="pointer"
onClick={(e) => {
setIsFormattedBalance(!isFormattedBalance);
e.stopPropagation();
}}
>
{output.amount}
</span>
</div>
)}
{showCopyAmount && <CopyButton copy={output.amount} />}
</div>
{isExpanded && (
<div className="output padding-l-t left-border">
{(output as CommonOutput).unlockConditions?.map(
(unlockCondition, idx) => (
<UnlockConditionView
key={idx}
unlockCondition={unlockCondition}
isPreExpanded={true}
/>
),
)}
</div>
)}
</div>
);
};

function getOutputTypeName(type: OutputType): string {
switch (type) {
case OutputType.Basic:
return "Basic";
case OutputType.Account:
return "Account";
case OutputType.Anchor:
return "Anchor";
case OutputType.Foundry:
return "Foundry";
case OutputType.Nft:
return "Nft";
case OutputType.Delegation:
return "Delegation";
}
}

export default OutputView;
Loading

0 comments on commit 98e6b13

Please sign in to comment.