diff --git a/src/app/(sidebar)/transaction/build/components/Operations.tsx b/src/app/(sidebar)/transaction/build/components/Operations.tsx index ca9725a6..36806837 100644 --- a/src/app/(sidebar)/transaction/build/components/Operations.tsx +++ b/src/app/(sidebar)/transaction/build/components/Operations.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChangeEvent, useEffect, useState } from "react"; +import { ChangeEvent, Fragment, useEffect, useState } from "react"; import { Badge, Button, @@ -8,6 +8,7 @@ import { Icon, Select, Notification, + Input, } from "@stellar/design-system"; import { formComponentTemplateTxnOps } from "@/components/formComponentTemplateTxnOps"; @@ -21,6 +22,7 @@ import { arrayItem } from "@/helpers/arrayItem"; import { isEmptyObject } from "@/helpers/isEmptyObject"; import { sanitizeObject } from "@/helpers/sanitizeObject"; import { shareableUrl } from "@/helpers/shareableUrl"; +import { getClaimableBalanceIdFromXdr } from "@/helpers/getClaimableBalanceIdFromXdr"; import { OP_SET_TRUST_LINE_FLAGS } from "@/constants/settings"; import { TRANSACTION_OPERATIONS } from "@/constants/transactionOperations"; @@ -38,7 +40,7 @@ import { } from "@/types/types"; export const Operations = () => { - const { transaction } = useStore(); + const { transaction, network } = useStore(); const { operations: txnOperations, xdr: txnXdr } = transaction.build; const { updateBuildOperations, @@ -900,6 +902,31 @@ export const Operations = () => { ); }; + const renderClaimableBalanceId = (opIndex: number) => { + const balanceId = getClaimableBalanceIdFromXdr({ + xdr: txnXdr, + networkPassphrase: network.passphrase, + opIndex, + }); + + if (!balanceId) { + return null; + } + + return ( + + ); + }; + return ( @@ -986,19 +1013,25 @@ export const Operations = () => { }, }); case "claimants": - return component.render({ - ...baseProps, - onChange: ( - claimants: AnyObject[] | undefined, - ) => { - handleOperationParamChange({ - opIndex: idx, - opParam: input, - opValue: claimants, - opType: op.operation_type, - }); - }, - }); + return ( + + {component.render({ + ...baseProps, + onChange: ( + claimants: AnyObject[] | undefined, + ) => { + handleOperationParamChange({ + opIndex: idx, + opParam: input, + opValue: claimants, + opType: op.operation_type, + }); + }, + })} + + {renderClaimableBalanceId(idx)} + + ); case "line": return component.render({ ...baseProps, diff --git a/src/app/(sidebar)/xdr/view/page.tsx b/src/app/(sidebar)/xdr/view/page.tsx index d32a466c..a8a9322b 100644 --- a/src/app/(sidebar)/xdr/view/page.tsx +++ b/src/app/(sidebar)/xdr/view/page.tsx @@ -9,12 +9,13 @@ import { Loader, Button, Icon, + Input, } from "@stellar/design-system"; +import { TransactionBuilder } from "@stellar/stellar-sdk"; import { useQueryClient } from "@tanstack/react-query"; import { stringify } from "lossless-json"; import { useLatestTxn } from "@/query/useLatestTxn"; -import * as StellarXdr from "@/helpers/StellarXdr"; import { XDR_TYPE_TRANSACTION_ENVELOPE } from "@/constants/settings"; import { Box } from "@/components/layout/Box"; @@ -23,13 +24,15 @@ import { XdrPicker } from "@/components/FormElements/XdrPicker"; import { XdrTypeSelect } from "@/components/XdrTypeSelect"; import { PrettyJsonTransaction } from "@/components/PrettyJsonTransaction"; import { TransactionHashReadOnlyField } from "@/components/TransactionHashReadOnlyField"; +import { LabelHeading } from "@/components/LabelHeading"; import { CopyJsonPayloadButton } from "@/components/CopyJsonPayloadButton"; +import * as StellarXdr from "@/helpers/StellarXdr"; import { parseToLosslessJson } from "@/helpers/parseToLosslessJson"; -import { useIsXdrInit } from "@/hooks/useIsXdrInit"; -import { useStore } from "@/store/useStore"; import { delayedAction } from "@/helpers/delayedAction"; import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; +import { useIsXdrInit } from "@/hooks/useIsXdrInit"; +import { useStore } from "@/store/useStore"; export default function ViewXdr() { const { xdr, network } = useStore(); @@ -78,6 +81,9 @@ export default function ViewXdr() { }; const xdrJsonDecoded = xdrDecodeJson(); + const txn = xdrJsonDecoded?.jsonString + ? TransactionBuilder.fromXDR(xdr.blob, network.passphrase) + : null; const prettifyJsonString = (jsonString: string): string => { try { @@ -88,6 +94,60 @@ export default function ViewXdr() { } }; + const renderClaimableBalanceIds = () => { + if (!txn) { + return null; + } + + const createClaimableBalanceIds = txn.operations.reduce( + (res, curOp, curIdx) => { + if (curOp.type === "createClaimableBalance") { + return [ + ...res, + { opIdx: curIdx, cbId: (txn as any).getClaimableBalanceId(curIdx) }, + ]; + } + + return res; + }, + [] as { opIdx: number; cbId: string }[], + ); + + if (createClaimableBalanceIds.length > 0) { + const labelText = + createClaimableBalanceIds.length === 1 + ? "a Create Claimable Balance operation" + : "Create Claimable Balance operations"; + + const idText = createClaimableBalanceIds.length === 1 ? "ID" : "IDs"; + + return ( + + {`This transaction contains ${labelText} with the following ${idText}`} + <> + {createClaimableBalanceIds.map((op) => { + const id = `view-xdr-ccb-op-${op.cbId}`; + + return ( + + ); + })} + + + ); + } + + return null; + }; + return (
@@ -172,6 +232,8 @@ export default function ViewXdr() { <> {xdrJsonDecoded?.jsonString ? ( + <>{renderClaimableBalanceIds()} +
{ @@ -171,6 +172,7 @@ const ClaimantPredicatePicker = ({ /> ) : ( renderComponent({ + parentId: id, index, nodes: transformPredicateDataForRender(predicateValue), onUpdate: handleUpdate, @@ -183,11 +185,13 @@ const ClaimantPredicatePicker = ({ }; const renderComponent = ({ + parentId, index, nodes, onUpdate, error, }: { + parentId: string; index: number; nodes: AnyObject[]; onUpdate: (val: AnyObject | undefined) => void; @@ -203,6 +207,7 @@ const renderComponent = ({ return ( { onUpdate({ @@ -273,7 +280,13 @@ const Predicate = ({ <> {isConditional && - renderComponent({ nodes: nodeValue || [], onUpdate, error, index })} + renderComponent({ + nodes: nodeValue || [], + onUpdate, + error, + index, + parentId, + })} ); @@ -281,6 +294,7 @@ const Predicate = ({ const PredicateType = ({ index, + parentId, parentPath, type, nodeValue, @@ -288,6 +302,7 @@ const PredicateType = ({ error, }: { index: number; + parentId: string; parentPath: string; type: string; nodeValue: AnyObject[]; @@ -299,7 +314,7 @@ const PredicateType = ({ return ( { @@ -332,7 +347,15 @@ const PredicateType = ({ ["and", "or"].includes(type) ? "PredicateWrapper__split" : "" } > - <>{renderComponent({ nodes: nodeValue, onUpdate, error, index })} + <> + {renderComponent({ + nodes: nodeValue, + onUpdate, + error, + index, + parentId, + })} + )} @@ -342,6 +365,7 @@ const PredicateType = ({ const PredicateTimeType = ({ index, + parentId, parentPath, type, nodeValue, @@ -349,6 +373,7 @@ const PredicateTimeType = ({ error, }: { index: number; + parentId: string; parentPath: string; type: string; nodeValue: AnyObject[]; @@ -358,7 +383,7 @@ const PredicateTimeType = ({ return ( <> { @@ -376,19 +401,21 @@ const PredicateTimeType = ({ {nodeValue && nodeValue.length > 0 && - renderComponent({ nodes: nodeValue, onUpdate, error, index })} + renderComponent({ nodes: nodeValue, onUpdate, error, index, parentId })} ); }; const PredicateTimeValue = ({ index, + parentId, parentPath, nodeValue, onUpdate, error, }: { index: number; + parentId: string; parentPath: string; nodeValue: string; onUpdate: (val: { @@ -412,7 +439,7 @@ const PredicateTimeValue = ({ {inputType === "absolute" && ( <> { + if (!(xdr && networkPassphrase)) { + return ""; + } + + const txn = TransactionBuilder.fromXDR(xdr, networkPassphrase); + let balanceId = ""; + + try { + balanceId = (txn as any).getClaimableBalanceId(opIndex); + } catch (e) { + // Do nothing + } + + return balanceId; +};