diff --git a/client/src/app/components/stardust/Input.tsx b/client/src/app/components/stardust/Input.tsx index 8fa0dcfb2..2c5be3314 100644 --- a/client/src/app/components/stardust/Input.tsx +++ b/client/src/app/components/stardust/Input.tsx @@ -2,14 +2,15 @@ /* eslint-disable jsdoc/require-returns */ import { Utils } from "@iota/sdk-wasm/web"; import classNames from "classnames"; -import React, { useContext, useState } from "react"; -import { useHistory, Link } from "react-router-dom"; -import Bech32Address from "./address/Bech32Address"; -import Output from "./Output"; +import React, { useContext, useEffect, useState } from "react"; +import { Link, useHistory } from "react-router-dom"; import DropdownIcon from "~assets/dropdown-arrow.svg?react"; import { formatAmount } from "~helpers/stardust/valueFormatHelper"; import { IInput } from "~models/api/stardust/IInput"; +import { IPreExpandedConfig } from "~models/components"; import NetworkContext from "../../context/NetworkContext"; +import Output from "./Output"; +import Bech32Address from "./address/Bech32Address"; interface InputProps { /** @@ -20,17 +21,25 @@ interface InputProps { * The network in context. */ readonly network: string; + /** + * Should the input be pre-expanded. + */ + readonly preExpandedConfig?: IPreExpandedConfig; } /** * Component which will display an Input on stardust. */ -const Input: React.FC = ({ input, network }) => { +const Input: React.FC = ({ input, network, preExpandedConfig }) => { const history = useHistory(); const { tokenInfo } = useContext(NetworkContext); - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded, setIsExpanded] = useState(preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.isPreExpanded ?? false); const [isFormattedBalance, setIsFormattedBalance] = useState(true); + useEffect(() => { + setIsExpanded(preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.isPreExpanded ?? isExpanded ?? false); + }, [preExpandedConfig]); + const fallbackInputView = (
setIsExpanded(!isExpanded)}> @@ -89,6 +98,7 @@ const Input: React.FC = ({ input, network }) => { amount={Number(input.output.output.amount)} network={network} showCopyAmount={true} + preExpandedConfig={preExpandedConfig} /> ) : ( fallbackInputView diff --git a/client/src/app/components/stardust/Output.tsx b/client/src/app/components/stardust/Output.tsx index 65af6ff5b..ceaba4cb6 100644 --- a/client/src/app/components/stardust/Output.tsx +++ b/client/src/app/components/stardust/Output.tsx @@ -61,17 +61,25 @@ class Output extends Component { super(props); this.state = { - isExpanded: this.props.isPreExpanded ?? false, + isExpanded: this.props.preExpandedConfig?.isAllPreExpanded ?? this.props.preExpandedConfig?.isPreExpanded ?? false, isFormattedBalance: true, }; } + componentDidUpdate(prevProps: Readonly): void { + if (prevProps.preExpandedConfig !== this.props.preExpandedConfig) { + this.setState({ + isExpanded: this.props.preExpandedConfig?.isAllPreExpanded ?? this.props.preExpandedConfig?.isPreExpanded ?? false, + }); + } + } + /** * Render the component. * @returns The node to render. */ public render(): ReactNode { - const { outputId, output, amount, showCopyAmount, network, isPreExpanded, displayFullOutputId, isLinksDisabled } = this.props; + const { outputId, output, amount, showCopyAmount, network, preExpandedConfig, displayFullOutputId, isLinksDisabled } = this.props; const { isExpanded, isFormattedBalance } = this.state; const tokenInfo: INodeInfoBaseToken = this.context.tokenInfo; @@ -223,33 +231,75 @@ class Output extends Component { {/* all output types except Treasury have common output conditions */} {output.type !== OutputType.Treasury && ( - {(output as CommonOutput).unlockConditions.map((unlockCondition, idx) => ( - - ))} - {(output as CommonOutput).features?.map((feature, idx) => ( - - ))} + {(output as CommonOutput).unlockConditions.map((unlockCondition, idx) => { + const isPreExpanded = + preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.unlockConditions?.[idx] ?? false; + return ; + })} + {(output as CommonOutput).features?.map((feature, idx) => { + const isPreExpanded = + preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.features?.[idx] ?? false; + return ( + + ); + })} {output.type === OutputType.Alias && - (output as AliasOutput).immutableFeatures?.map((immutableFeature, idx) => ( - - ))} + (output as AliasOutput).immutableFeatures?.map((immutableFeature, idx) => { + const isPreExpanded = + preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.immutableFeatures?.[idx] ?? false; + return ( + + ); + })} {output.type === OutputType.Nft && - (output as NftOutput).immutableFeatures?.map((immutableFeature, idx) => ( - - ))} + (output as NftOutput).immutableFeatures?.map((immutableFeature, idx) => { + const isPreExpanded = + preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.immutableFeatures?.[idx] ?? false; + return ( + + ); + })} {output.type === OutputType.Foundry && - (output as FoundryOutput).immutableFeatures?.map((immutableFeature, idx) => ( - - ))} - {(output as CommonOutput).nativeTokens?.map((token, idx) => ( - - ))} + (output as FoundryOutput).immutableFeatures?.map((immutableFeature, idx) => { + const isPreExpanded = + preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.immutableFeatures?.[idx] ?? false; + return ( + + ); + })} + {(output as CommonOutput).nativeTokens?.map((token, idx) => { + const isPreExpanded = + preExpandedConfig?.isAllPreExpanded ?? preExpandedConfig?.nativeTokens?.[idx] ?? false; + return ( + + ); + })} )}
diff --git a/client/src/app/components/stardust/OutputProps.tsx b/client/src/app/components/stardust/OutputProps.tsx index 6c76dbe03..edd9d1382 100644 --- a/client/src/app/components/stardust/OutputProps.tsx +++ b/client/src/app/components/stardust/OutputProps.tsx @@ -1,4 +1,5 @@ import { Output } from "@iota/sdk-wasm/web"; +import { IPreExpandedConfig } from "~models/components"; export interface OutputProps { /** @@ -21,11 +22,6 @@ export interface OutputProps { */ showCopyAmount: boolean; - /** - * Should the output be pre-expanded. - */ - isPreExpanded?: boolean; - /** * Should the outputId be displayed in full (default truncated). */ @@ -40,4 +36,9 @@ export interface OutputProps { * Disable links if block is conflicting. */ isLinksDisabled?: boolean; + + /** + * Should the output and its fields be pre-expanded. + */ + preExpandedConfig?: IPreExpandedConfig; } diff --git a/client/src/app/components/stardust/block/payload/TransactionPayload.tsx b/client/src/app/components/stardust/block/payload/TransactionPayload.tsx index a7a023fe4..3a1d5febf 100644 --- a/client/src/app/components/stardust/block/payload/TransactionPayload.tsx +++ b/client/src/app/components/stardust/block/payload/TransactionPayload.tsx @@ -1,104 +1,80 @@ -import React, { ReactNode } from "react"; -import { TransactionPayloadProps } from "./TransactionPayloadProps"; -import { TransactionPayloadState } from "./TransactionPayloadState"; +import React, { useContext, useEffect, useState } from "react"; +import NetworkContext from "~/app/context/NetworkContext"; import transactionPayloadMessage from "~assets/modals/stardust/block/transaction-payload.json"; -import NetworkContext from "../../../../context/NetworkContext"; -import AsyncComponent from "../../../AsyncComponent"; +import { getInputsPreExpandedConfig, getOutputsPreExpandedConfig } from "~helpers/stardust/preExpandedConfig"; +import { IPreExpandedConfig } from "~models/components"; import Modal from "../../../Modal"; import Input from "../../Input"; import Output from "../../Output"; import Unlocks from "../../Unlocks"; import "./TransactionPayload.scss"; +import { TransactionPayloadProps } from "./TransactionPayloadProps"; /** * Component which will display a transaction payload. */ -class TransactionPayload extends AsyncComponent { - /** - * The component context type. - */ - public static contextType = NetworkContext; - - /** - * The component context. - */ - public declare context: React.ContextType; - - /** - * Create a new instance of TransactionPayload. - * @param props The props. - */ - constructor(props: TransactionPayloadProps) { - super(props); - - this.state = { - isFormattedBalance: true, - }; - } +const TransactionPayload: React.FC = ({ network, inputs, unlocks, outputs, header, isLinksDisabled }) => { + const [inputsPreExpandedConfig, setInputsPreExpandedConfig] = useState([]); + const { bech32Hrp } = useContext(NetworkContext); - /** - * The component mounted. - */ - public async componentDidMount(): Promise { - super.componentDidMount(); - } + const outputsPreExpandedConfig = getOutputsPreExpandedConfig(outputs); - /** - * Render the component. - * @returns The node to render. - */ - public render(): ReactNode { - const { network, inputs, unlocks, outputs, header, isLinksDisabled } = this.props; + useEffect(() => { + if (bech32Hrp) { + const inputsPreExpandedConfig = getInputsPreExpandedConfig(inputs, unlocks, bech32Hrp); + setInputsPreExpandedConfig(inputsPreExpandedConfig); + } + }, [bech32Hrp]); - return ( -
- {header && ( -
-
-

{header}

- -
+ return ( +
+ {header && ( +
+
+

{header}

+
- )} -
-
-
-

From

- - {inputs.length} -
-
- {inputs.map((input, idx) => ( - - ))} - -
+
+ )} +
+
+
+

From

+ + {inputs.length} +
+
+ {inputs.map((input, idx) => ( + + ))} +
+
-
-
-

To

- - {outputs.length} -
-
- {outputs.map((output, idx) => ( - - ))} -
+
+
+

To

+ + {outputs.length} +
+
+ {outputs.map((output, idx) => ( + + ))}
- ); - } -} +
+ ); +}; export default TransactionPayload; diff --git a/client/src/app/routes/stardust/OutputList.tsx b/client/src/app/routes/stardust/OutputList.tsx index cd6d732a4..fed1fe2d9 100644 --- a/client/src/app/routes/stardust/OutputList.tsx +++ b/client/src/app/routes/stardust/OutputList.tsx @@ -72,7 +72,7 @@ const OutputList: React.FC> = ({ output={item.outputDetails.output} amount={Number(item.outputDetails.output.amount)} showCopyAmount={true} - isPreExpanded={false} + preExpandedConfig={{ isPreExpanded: false }} />
))} @@ -108,7 +108,7 @@ const OutputList: React.FC> = ({ output={item.outputDetails.output} amount={Number(item.outputDetails.output.amount)} showCopyAmount={true} - isPreExpanded={false} + preExpandedConfig={{ isPreExpanded: false }} />
))} diff --git a/client/src/app/routes/stardust/OutputPage.tsx b/client/src/app/routes/stardust/OutputPage.tsx index 03461cda6..4ec091e8d 100644 --- a/client/src/app/routes/stardust/OutputPage.tsx +++ b/client/src/app/routes/stardust/OutputPage.tsx @@ -72,7 +72,7 @@ const OutputPage: React.FC> = ({ output={output} amount={Number(output.amount)} showCopyAmount={true} - isPreExpanded={true} + preExpandedConfig={{ isAllPreExpanded: true }} />
diff --git a/client/src/helpers/stardust/preExpandedConfig.ts b/client/src/helpers/stardust/preExpandedConfig.ts new file mode 100644 index 000000000..a1badc321 --- /dev/null +++ b/client/src/helpers/stardust/preExpandedConfig.ts @@ -0,0 +1,127 @@ +import { + AddressUnlockCondition, + CommonOutput, + ExpirationUnlockCondition, + GovernorAddressUnlockCondition, + ReferenceUnlock, + SignatureUnlock, + StateControllerAddressUnlockCondition, + Unlock, + UnlockConditionType, + UnlockType, + Utils, +} from "@iota/sdk-wasm/web"; +import { Bech32AddressHelper } from "~/helpers/stardust/bech32AddressHelper"; +import { IInput } from "~models/api/stardust/IInput"; +import { IOutput } from "~models/api/stardust/IOutput"; +import { IPreExpandedConfig } from "~models/components"; + +const OUTPUT_EXPAND_CONDITIONS: UnlockConditionType[] = [ + UnlockConditionType.Address, + UnlockConditionType.StateControllerAddress, + UnlockConditionType.GovernorAddress, +]; +const INPUT_EXPAND_CONDITIONS: UnlockConditionType[] = [...OUTPUT_EXPAND_CONDITIONS, UnlockConditionType.Expiration]; + +/** + * Get the preExpandedConfig for the inputs. + * Expand the input and its unlock conditions if, given the unlocks, we match unlock conditions. + * @param inputs The inputs to calculate the preExpandedConfig for. + * @param unlocks The unlocks used to spend the inputs. + * @param bech32Hrp The bech32 hrp. + * @returns The preExpandedConfig for the inputs. + */ +export function getInputsPreExpandedConfig(inputs: IInput[], unlocks: Unlock[], bech32Hrp: string): IPreExpandedConfig[] { + const inputsPreExpandedConfig: IPreExpandedConfig[] = inputs.map((input, idx) => { + const commonOutput = input?.output?.output as unknown as CommonOutput; + let preExpandedConfig: IPreExpandedConfig = {}; + if (commonOutput) { + const matchExpandCondition = commonOutput.unlockConditions?.find((unlockCondition) => + INPUT_EXPAND_CONDITIONS.includes(unlockCondition.type), + ); + preExpandedConfig = { + isPreExpanded: !!matchExpandCondition, + }; + if (input?.output?.output && "unlockConditions" in input.output.output) { + const commmonOutput = input.output.output as unknown as CommonOutput; + let unlock = unlocks[idx]; + if (unlock.type === UnlockType.Reference) { + const referenceUnlock = unlock as ReferenceUnlock; + unlock = unlocks[referenceUnlock.reference]; + } + const unlockSignatureAddress = Utils.hexPublicKeyToBech32Address( + (unlock as SignatureUnlock).signature.publicKey, + bech32Hrp, + ); + preExpandedConfig = { + ...preExpandedConfig, + unlockConditions: commmonOutput.unlockConditions?.map((unlockCondition) => { + switch (unlockCondition.type) { + case UnlockConditionType.Address: { + const unlockAddress = Bech32AddressHelper.buildAddress( + bech32Hrp, + (unlockCondition as AddressUnlockCondition).address, + )?.bech32; + return unlockAddress === unlockSignatureAddress; + } + case UnlockConditionType.Expiration: { + const unlockAddress = Bech32AddressHelper.buildAddress( + bech32Hrp, + (unlockCondition as ExpirationUnlockCondition).returnAddress, + )?.bech32; + return unlockAddress === unlockSignatureAddress; + } + case UnlockConditionType.StateControllerAddress: { + const unlockAddress = Bech32AddressHelper.buildAddress( + bech32Hrp, + (unlockCondition as StateControllerAddressUnlockCondition).address, + )?.bech32; + return unlockAddress === unlockSignatureAddress; + } + case UnlockConditionType.GovernorAddress: { + const unlockAddress = Bech32AddressHelper.buildAddress( + bech32Hrp, + (unlockCondition as GovernorAddressUnlockCondition).address, + )?.bech32; + return unlockAddress === unlockSignatureAddress; + } + default: + return false; + } + }), + }; + } + } + return preExpandedConfig; + }); + return inputsPreExpandedConfig; +} + +/** + * Get the preExpandedConfig for the outputs. + * Expand the output and its relevant receiver address related unlock conditions. + * @param outputs The outputs to calculate the preExpandedConfig for. + * @returns The preExpandedConfig for the outputs. + */ +export function getOutputsPreExpandedConfig(outputs: IOutput[]): IPreExpandedConfig[] { + const outputsPreExpandedConfig: IPreExpandedConfig[] = outputs.map((output) => { + const commonOutput = output.output as CommonOutput; + let preExpandedConfig: IPreExpandedConfig = {}; + if (commonOutput) { + const matchExpandCondition = commonOutput.unlockConditions?.find((unlockCondition) => + OUTPUT_EXPAND_CONDITIONS.includes(unlockCondition.type), + ); + preExpandedConfig = { + isPreExpanded: !!matchExpandCondition, + }; + preExpandedConfig = { + ...preExpandedConfig, + unlockConditions: commonOutput.unlockConditions?.map((unlockCondition) => + OUTPUT_EXPAND_CONDITIONS.includes(unlockCondition.type), + ), + }; + } + return preExpandedConfig; + }); + return outputsPreExpandedConfig; +} diff --git a/client/src/models/components/IPreExpandedConfig.tsx b/client/src/models/components/IPreExpandedConfig.tsx new file mode 100644 index 000000000..d12370b70 --- /dev/null +++ b/client/src/models/components/IPreExpandedConfig.tsx @@ -0,0 +1,10 @@ +export interface IPreExpandedConfig { + isPreExpanded?: boolean; + unlockConditions?: boolean[]; + features?: boolean[]; + immutableFeatures?: boolean[]; + nativeTokens?: boolean[]; + + // generic to expand all + isAllPreExpanded?: boolean; +} diff --git a/client/src/models/components/index.tsx b/client/src/models/components/index.tsx new file mode 100644 index 000000000..6d3e26606 --- /dev/null +++ b/client/src/models/components/index.tsx @@ -0,0 +1 @@ +export * from "./IPreExpandedConfig";