From 5a4b7afbd808de1a12fbe1fb7da716c8f58b8aa4 Mon Sep 17 00:00:00 2001 From: Jeesun Kim Date: Thu, 11 Jun 2026 13:39:23 -0700 Subject: [PATCH 1/6] Remove tab and add test for --- next.config.js | 5 + .../transaction/components/Signatures.tsx | 2 +- .../dashboard/components/Signatures.tsx | 4 +- .../transaction/sign/components/Import.tsx | 111 -- .../transaction/sign/components/Overview.tsx | 1020 ----------------- src/app/(sidebar)/transaction/sign/page.tsx | 17 - src/app/page.tsx | 4 +- src/components/DataTable/index.tsx | 4 +- src/constants/navItems.tsx | 4 - src/constants/routes.ts | 1 - tests/e2e/feeBumpPage.test.ts | 2 +- tests/e2e/importTransactionPage.test.ts | 227 ++++ tests/e2e/signStepContent.test.ts | 4 +- tests/e2e/signTransactionPage.test.ts | 435 ------- tests/e2e/signerSelector.test.ts | 2 +- 15 files changed, 243 insertions(+), 1599 deletions(-) delete mode 100644 src/app/(sidebar)/transaction/sign/components/Import.tsx delete mode 100644 src/app/(sidebar)/transaction/sign/components/Overview.tsx delete mode 100644 src/app/(sidebar)/transaction/sign/page.tsx create mode 100644 tests/e2e/importTransactionPage.test.ts delete mode 100644 tests/e2e/signTransactionPage.test.ts diff --git a/next.config.js b/next.config.js index 24d18788a..5a34efd5a 100644 --- a/next.config.js +++ b/next.config.js @@ -258,6 +258,11 @@ const nextConfig = { destination: "/transaction/build", permanent: true, }, + { + source: "/transaction/sign", + destination: "/transaction/import", + permanent: true, + }, ]; }, }; diff --git a/src/app/(sidebar)/transaction/components/Signatures.tsx b/src/app/(sidebar)/transaction/components/Signatures.tsx index 499289ec1..4ef293396 100644 --- a/src/app/(sidebar)/transaction/components/Signatures.tsx +++ b/src/app/(sidebar)/transaction/components/Signatures.tsx @@ -257,7 +257,7 @@ const EnvelopeSignaturesTable = ({ {rows.map((row, index) => ( - + {renderSigner(row.matchStatus, row.signerPubKey)} diff --git a/src/app/(sidebar)/transaction/dashboard/components/Signatures.tsx b/src/app/(sidebar)/transaction/dashboard/components/Signatures.tsx index 8da474f3c..bf151a908 100644 --- a/src/app/(sidebar)/transaction/dashboard/components/Signatures.tsx +++ b/src/app/(sidebar)/transaction/dashboard/components/Signatures.tsx @@ -80,7 +80,7 @@ export const Signatures = ({ ); return ( - + {signerPubKey ? renderSigner(isVerified, signerPubKey) : "-"} @@ -106,7 +106,7 @@ export const Signatures = ({ const isMatch = verifyAuthEntryPublicKey(entry.publicKey, entry.address); return ( - + {renderSigner(isMatch, entry.address)} diff --git a/src/app/(sidebar)/transaction/sign/components/Import.tsx b/src/app/(sidebar)/transaction/sign/components/Import.tsx deleted file mode 100644 index 89cdfc6af..000000000 --- a/src/app/(sidebar)/transaction/sign/components/Import.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Alert, Text, Button } from "@stellar/design-system"; -import { TransactionBuilder } from "@stellar/stellar-sdk"; - -import { useStore } from "@/store/useStore"; - -import { validate } from "@/validate"; -import { trackEvent, TrackingEvent } from "@/metrics/tracking"; - -import { Box } from "@/components/layout/Box"; -import { XdrPicker } from "@/components/FormElements/XdrPicker"; -import { PageCard } from "@/components/layout/PageCard"; - -export const Import = () => { - const { network, transaction } = useStore(); - const { updateSignActiveView, updateSignImportXdr, updateSignImportTx } = - transaction; - - const [txXdr, setTxXdr] = useState(""); - const [txErrMsg, setTxErrMsg] = useState(""); - const [txSuccessMsg, setTxSuccessMsg] = useState(""); - - // on textarea onChange for 'import tx xdr' - const onChange = (value: string) => { - // reset messages onChange - setTxErrMsg(""); - setTxSuccessMsg(""); - setTxXdr(value); - - if (value.length > 0) { - const validatedXDR = validate.getXdrError(value); - - if (validatedXDR?.result && validatedXDR.message) { - if (validatedXDR.result === "success") { - setTxSuccessMsg(validatedXDR.message); - } else { - setTxErrMsg(validatedXDR.message); - } - } - } - }; - - const onImport = () => { - try { - const transaction = TransactionBuilder.fromXDR(txXdr, network.passphrase); - - updateSignImportTx(transaction); - updateSignImportXdr(txXdr); - - // change to 'overview' view when successfully imported - updateSignActiveView("overview"); - - trackEvent(TrackingEvent.TRANSACTION_SIGN_IMPORT_SUCCESS); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { - setTxErrMsg("Unable to import a transaction envelope"); - updateSignImportXdr(""); - updateSignActiveView("import"); - - trackEvent(TrackingEvent.TRANSACTION_SIGN_IMPORT_ERROR); - } - }; - - return ( - - -
- onChange(e.target.value)} - /> - -
- -
-
-
- - - - The transaction signer lets you add signatures to a Stellar - transaction. Signatures are used in the network to prove that the - account is authorized to perform the operations in the transaction. - - - For simple transactions, you only need one signature from the correct - account. Some advanced transactions may require more than one - signature if there are multiple source accounts or signing keys. - - -
- ); -}; diff --git a/src/app/(sidebar)/transaction/sign/components/Overview.tsx b/src/app/(sidebar)/transaction/sign/components/Overview.tsx deleted file mode 100644 index 7f1541738..000000000 --- a/src/app/(sidebar)/transaction/sign/components/Overview.tsx +++ /dev/null @@ -1,1020 +0,0 @@ -"use client"; - -import { useEffect, useRef, useState } from "react"; -import { Icon, Button, Select } from "@stellar/design-system"; -import { - FeeBumpTransaction, - Transaction, - TransactionBuilder, - xdr, -} from "@stellar/stellar-sdk"; -import { useRouter } from "next/navigation"; - -import { FEE_BUMP_TX_FIELDS, TX_FIELDS } from "@/constants/signTransactionPage"; -import { XDR_TYPE_TRANSACTION_ENVELOPE } from "@/constants/settings"; -import { Routes } from "@/constants/routes"; - -import { useStore } from "@/store/useStore"; - -import { txHelper } from "@/helpers/txHelper"; -import { delayedAction } from "@/helpers/delayedAction"; -import { shortenStellarAddress } from "@/helpers/shortenStellarAddress"; -import { arrayItem } from "@/helpers/arrayItem"; -import { scrollElIntoView } from "@/helpers/scrollElIntoView"; -import { isSorobanOperationType } from "@/helpers/sorobanUtils"; - -import { useSignWithExtensionWallet } from "@/hooks/useSignWithExtensionWallet"; - -import { validate } from "@/validate"; -import { trackEvent, TrackingEvent } from "@/metrics/tracking"; - -import { Box } from "@/components/layout/Box"; -import { MultiPicker } from "@/components/FormElements/MultiPicker"; -import { TextPicker } from "@/components/FormElements/TextPicker"; -import { ValidationResponseCard } from "@/components/ValidationResponseCard"; -import { XdrPicker } from "@/components/FormElements/XdrPicker"; -import { ViewInXdrButton } from "@/components/ViewInXdrButton"; -import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; -import { LabelHeading } from "@/components/LabelHeading"; -import { PageCard } from "@/components/layout/PageCard"; -import { MessageField } from "@/components/MessageField"; - -const MIN_LENGTH_FOR_FULL_WIDTH_FIELD = 30; - -type TxSignatureType = - | "secretKey" - | "hardwareWallet" - | "extensionWallet" - | "signature"; - -export const Overview = () => { - const { network, transaction, xdr, walletKit } = useStore(); - const { - sign, - updateSignActiveView, - updateSignImportTx, - updateSignedTx, - updateBipPath, - resetSign, - updateFeeBumpParams, - } = transaction; - - const router = useRouter(); - const successResponseEl = useRef(null); - - const [signError, setSignError] = useState(""); - - // Secret key - const [secretKeyInputs, setSecretKeyInputs] = useState([""]); - const [secretKeySignature, setSecretKeySignature] = useState< - xdr.DecoratedSignature[] - >([]); - const [secretKeySuccessMsg, setSecretKeySuccessMsg] = useState(""); - const [secretKeyErrorMsg, setSecretKeyErrorMsg] = useState(""); - - // Hardware wallet - const [hardwareSignature, setHardwareSignature] = useState< - xdr.DecoratedSignature[] - >([]); - const [hardwareSuccessMsg, setHardwareSuccessMsg] = useState(""); - const [hardwareErrorMsg, setHardwareErrorMsg] = useState(""); - - const [selectedHardware, setSelectedHardware] = useState(""); - const [isHardwareLoading, setIsHardwareLoading] = useState(false); - const [bipPathErrorMsg, setBipPathErrorMsg] = useState(""); - - // Extension wallet - const [extensionSignature, setExtensionSignature] = useState< - xdr.DecoratedSignature[] - >([]); - - const [isExtensionLoading, setIsExtensionLoading] = useState(false); - const [isExtensionClear, setIsExtensionClear] = useState(false); - - type SigObj = { - publicKey: string; - signature: string; - }; - - const sigObj: SigObj = { - publicKey: "", - signature: "", - }; - - // Signature - const [sigInputs, setSigInputs] = useState([sigObj]); - const [sigInputsError, setSigInputsError] = useState([sigObj]); - const [sigSignature, setSigSignature] = useState( - [], - ); - const [sigSuccessMsg, setSigSuccessMsg] = useState(""); - const [sigErrorMsg, setSigErrorMsg] = useState(""); - - const [isSorobanXdr, setIsSorobanXdr] = useState(false); - - const HAS_SECRET_KEYS = secretKeyInputs.some((input) => input !== ""); - const HAS_INVALID_SECRET_KEYS = secretKeyInputs.some((input) => { - if (input.length) { - return validate.getSecretKeyError(input); - } - return false; - }); - - const { - signedTxXdr: exSignedTxXdr, - successMsg: exSuccessMsg, - errorMsg: exErrorMsg, - } = useSignWithExtensionWallet({ - isEnabled: isExtensionLoading, - isClear: isExtensionClear, - txXdr: sign.importXdr, - }); - - useEffect(() => { - if (sign.importXdr) { - // used to persist page data when accessed by query string - const transaction = TransactionBuilder.fromXDR( - sign.importXdr, - network.passphrase, - ); - - const isSorobanTx = isSorobanOperationType( - transaction?.operations?.[0]?.type, - ); - - setIsSorobanXdr(isSorobanTx); - updateSignImportTx(transaction); - } else { - updateSignActiveView("import"); - } - }, [ - network.passphrase, - sign.importXdr, - updateSignActiveView, - updateSignImportTx, - ]); - - useEffect(() => { - // Always reset the signed tx since user's signature(s) always get reset as well - updateSignedTx(""); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - if (exSuccessMsg || exErrorMsg) { - handleSign({ sigType: "extensionWallet", isClear: false }); - setIsExtensionLoading(false); - } - // Not including handleSign - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [exErrorMsg, exSuccessMsg]); - - const onViewSubmitTxn = () => { - if (sign.signedTx) { - xdr.updateXdrBlob(sign.signedTx); - xdr.updateXdrType(XDR_TYPE_TRANSACTION_ENVELOPE); - - delayedAction({ - action: () => { - trackEvent(TrackingEvent.TRANSACTION_SIGN_SUBMIT_IN_TX_SUBMITTER); - router.push(Routes.SUBMIT_TRANSACTION); - }, - delay: 200, - }); - } - }; - - const onSimulateTxn = () => { - if (sign.signedTx) { - xdr.updateXdrBlob(sign.signedTx); - xdr.updateXdrType(XDR_TYPE_TRANSACTION_ENVELOPE); - - transaction.updateSimulateTriggerOnLaunch(true); - - // Adding delay to make sure the store will update - delayedAction({ - action: () => { - trackEvent(TrackingEvent.TRANSACTION_SIGN_SIMULATE); - router.push(Routes.SIMULATE_TRANSACTION); - }, - delay: 200, - }); - } - }; - - const onWrapWithFeeBump = () => { - if (sign.signedTx) { - updateFeeBumpParams({ xdr: sign.signedTx }); - - delayedAction({ - action: () => { - trackEvent(TrackingEvent.TRANSACTION_SIGN_FEE_BUMP); - router.push(Routes.FEE_BUMP_TRANSACTION); - }, - delay: 200, - }); - } - }; - - const handleSign = async ({ - sigType, - isClear, - }: { - sigType: TxSignatureType; - isClear?: boolean; - }) => { - setSignError(""); - - // Initial state - let secretKeySigs: xdr.DecoratedSignature[] = []; - let hardwareSigs: xdr.DecoratedSignature[] = []; - let extensionSigs: xdr.DecoratedSignature[] = []; - let signatureSigs: xdr.DecoratedSignature[] = []; - - try { - switch (sigType) { - case "secretKey": - secretKeySigs = signSecretKey(isClear).signature; - break; - case "hardwareWallet": - hardwareSigs = (await signHardwareWallet(isClear)).signature; - break; - case "extensionWallet": - if (!isClear && exSignedTxXdr) { - extensionSigs = - txHelper.extractLastSignature({ - txXdr: exSignedTxXdr, - networkPassphrase: network.passphrase, - }) || []; - - setExtensionSignature(extensionSigs); - } else { - setExtensionSignature([]); - } - - break; - case "signature": - signatureSigs = signSignature(isClear).signature; - break; - default: - // Do nothing - } - - // Previously added signatures from other types - if (sigType !== "secretKey" && secretKeySuccessMsg) { - secretKeySigs = secretKeySignature; - } - - if (sigType !== "hardwareWallet" && hardwareSuccessMsg) { - hardwareSigs = hardwareSignature; - } - - if (sigType !== "extensionWallet" && exSuccessMsg) { - extensionSigs = extensionSignature; - } - - if (sigType !== "signature" && sigSuccessMsg) { - signatureSigs = sigSignature; - } - - const tx = TransactionBuilder.fromXDR(sign.importXdr, network.passphrase); - const allSigs = [ - ...secretKeySigs, - ...hardwareSigs, - ...extensionSigs, - ...signatureSigs, - ]; - - if (allSigs.length > 0) { - tx.signatures.push(...allSigs); - - const signedTx = tx.toEnvelope().toXDR("base64"); - updateSignedTx(signedTx); - - scrollElIntoView(successResponseEl); - } else { - updateSignedTx(""); - } - } catch (e: any) { - setSignError(e.toString()); - } - }; - - const handleSignatureOnChange = ( - value: string, - key: "publicKey" | "signature", - index: number, - ) => { - const validationError = - key === "publicKey" ? validate.getPublicKeyError(value) : ""; - - const inputVal = arrayItem.update(sigInputs, index, { - ...sigInputs[index], - [key]: value, - }); - - const inputError = arrayItem.update(sigInputsError, index, { - ...sigInputsError[index], - publicKey: validationError, - }); - - updateSignatureInputsAndError(inputVal, inputError); - }; - - const updateSignatureInputsAndError = (inputs: SigObj[], error: SigObj[]) => { - setSigInputs(inputs); - setSigInputsError(error); - - if (sigSuccessMsg || sigErrorMsg) { - handleSign({ sigType: "signature", isClear: true }); - } - }; - - const signSecretKey = (isClear?: boolean) => { - let signature: xdr.DecoratedSignature[] = []; - let successMsg = ""; - let errorMsg = ""; - - if (!isClear) { - const txSig = txHelper.secretKeySignature({ - txXdr: sign.importXdr, - networkPassphrase: network.passphrase, - signers: secretKeyInputs, - }); - - signature = txSig.signature; - successMsg = txSig.successMsg || ""; - errorMsg = txSig.errorMsg || ""; - } - - setSecretKeySignature(signature); - setSecretKeySuccessMsg(successMsg); - setSecretKeyErrorMsg(errorMsg); - - if (successMsg) { - trackEvent(TrackingEvent.TRANSACTION_SIGN_SECRET_KEY_SUCCESS); - } - - if (errorMsg) { - trackEvent(TrackingEvent.TRANSACTION_SIGN_SECRET_KEY_ERROR); - } - - return { signature, errorMsg }; - }; - - const signHardwareWallet = async (isClear?: boolean) => { - setIsHardwareLoading(true); - - let signature: xdr.DecoratedSignature[] = []; - let successMsg = ""; - let errorMsg = ""; - - if (!isClear) { - try { - if (selectedHardware === "ledger") { - const { signature: ledgerSig, error } = await txHelper.signWithLedger( - { - bipPath: sign.bipPath, - transaction: sign.importTx as FeeBumpTransaction | Transaction, - isHash: false, - }, - ); - - signature = ledgerSig ?? []; - - errorMsg = error || ""; - - if (errorMsg.includes("0x6511")) { - errorMsg = - "Please select Stellar app on the Ledger device and try again"; - } - } - - if (selectedHardware === "ledger_hash") { - const { signature: ledgerSig, error } = await txHelper.signWithLedger( - { - bipPath: sign.bipPath, - transaction: sign.importTx as FeeBumpTransaction | Transaction, - isHash: true, - }, - ); - - signature = ledgerSig ?? []; - errorMsg = error || ""; - } - - if (selectedHardware === "trezor") { - const path = `m/${sign.bipPath}`; - - const { signature: trezorSig, error } = await txHelper.signWithTrezor( - { - bipPath: path, - transaction: sign.importTx as Transaction, - }, - ); - - signature = trezorSig ?? []; - errorMsg = error || ""; - } - - successMsg = errorMsg - ? "" - : "Successfully added a hardware wallet signature"; - } catch (err) { - errorMsg = `An unexpected error occurred: ${err}`; - } - } - - setIsHardwareLoading(false); - setHardwareSignature(signature); - setHardwareSuccessMsg(successMsg); - setHardwareErrorMsg(errorMsg); - - if (successMsg) { - trackEvent(TrackingEvent.TRANSACTION_SIGN_HARDWARE_SUCCESS, { - selectedHardware, - }); - } - - if (errorMsg) { - trackEvent(TrackingEvent.TRANSACTION_SIGN_HARDWARE_ERROR, { - selectedHardware, - }); - } - - return { signature, errorMsg }; - }; - - const signSignature = (isClear?: boolean) => { - let signature: xdr.DecoratedSignature[] = []; - let successMsg = ""; - let errorMsg = ""; - - if (!isClear) { - const existingSigs = sign.importXdr - ? txHelper.extractSignaturesFromTx({ - txXdr: sign.importXdr, - networkPassphrase: network.passphrase, - }) - : []; - - const txSig = txHelper.decoratedSigFromHexSig(sigInputs, existingSigs); - - signature = txSig.signature; - successMsg = txSig.successMsg || ""; - errorMsg = txSig.errorMsg || ""; - } - - setSigSignature(signature); - setSigSuccessMsg(successMsg); - setSigErrorMsg(errorMsg); - - if (successMsg) { - trackEvent(TrackingEvent.TRANSACTION_SIGN_SIGNATURE_SUCCESS, { - selectedHardware, - }); - } - - if (errorMsg) { - trackEvent(TrackingEvent.TRANSACTION_SIGN_SIGNATURE_ERROR, { - selectedHardware, - }); - } - - return { signature, errorMsg }; - }; - - const REQUIRED_FIELDS = [ - { - label: "Signing for", - value: network.passphrase, - }, - { - label: "Transaction envelope XDR", - value: sign.importXdr, - }, - { - label: "Transaction hash", - value: sign.importTx?.hash().toString("hex"), - }, - ]; - - let mergedFields; - - if (sign.importTx instanceof FeeBumpTransaction) { - mergedFields = [...REQUIRED_FIELDS, ...FEE_BUMP_TX_FIELDS(sign.importTx)]; - } else if (sign.importTx) { - mergedFields = [...REQUIRED_FIELDS, ...TX_FIELDS(sign.importTx)]; - } - - const SignTxButton = ({ - label = "Sign transaction", - onSign, - onClear, - isDisabled, - isLoading, - successMsg, - errorMsg, - }: { - label?: string; - onSign: () => void; - onClear: () => void; - isDisabled?: boolean; - isLoading?: boolean; - successMsg: string; - errorMsg: string; - }) => { - return ( - - {successMsg ? ( - - ) : ( - - )} - - <> - {successMsg || errorMsg ? ( - - ) : null} - - - ); - }; - - const AddSignatureButton = () => { - const signatureText = sigInputs.length === 1 ? "signature" : "signatures"; - - const hasEmptyFields = - sigInputs.reduce((res, cur) => { - if (!(cur.publicKey && cur.signature)) { - return [...res, true]; - } - - return res; - }, [] as boolean[]).length > 0; - - const hasErrors = - sigInputsError.reduce((res, cur) => { - if (cur.publicKey || cur.signature) { - return [...res, true]; - } - - return res; - }, [] as boolean[]).length > 0; - - return ( - - {sigSuccessMsg ? ( - - ) : ( - - )} - - - - <> - {sigSuccessMsg || sigErrorMsg ? ( - - ) : null} - - - ); - }; - - const getAllSigsMessage = () => { - const allMsgs = []; - const secretKeySigCount = secretKeySignature.length; - const hardwareSigCount = hardwareSignature.length; - const extensionSigCount = extensionSignature.length; - const sigSignatureCount = sigSignature.length; - - const getMsg = (count: number, label: string) => - `${count} ${label} signature${count > 1 ? "s" : ""}`; - - if (secretKeySigCount > 0) { - allMsgs.push(getMsg(secretKeySigCount, "secret key")); - } - - if (hardwareSigCount > 0) { - allMsgs.push(getMsg(hardwareSigCount, "hardware wallet")); - } - - if (extensionSigCount > 0) { - allMsgs.push(getMsg(extensionSigCount, "extension wallet")); - } - - if (sigSignatureCount > 0) { - allMsgs.push(getMsg(sigSignatureCount, "")); - } - - if (allMsgs.length > 0) { - return `${allMsgs.join(", ")} added`; - } - - return ""; - }; - - return ( - <> - } - iconPosition="right" - onClick={() => { - resetSign(); - trackEvent(TrackingEvent.TRANSACTION_SIGN_CLEAR); - }} - > - Clear and import new - - } - > -
- {mergedFields?.map((field) => { - const className = - field.value && - field.value.toString().length >= MIN_LENGTH_FOR_FULL_WIDTH_FIELD - ? "full-width" - : "half-width"; - - if (field.label.includes("XDR")) { - return ( -
- -
- ); - } else { - return ( -
- -
- ); - } - })} -
-
- -
- - - - { - if (secretKeySuccessMsg || secretKeyErrorMsg) { - handleSign({ sigType: "secretKey", isClear: true }); - } - setSecretKeyInputs(val); - }} - validate={validate.getSecretKeyError} - placeholder="Secret key (starting with S) or hash preimage (in hex)" - autocomplete="off" - useSecretSelector={true} - isPassword - /> - { - handleSign({ sigType: "secretKey", isClear: false }); - }} - onClear={() => { - handleSign({ sigType: "secretKey", isClear: true }); - }} - isDisabled={!HAS_SECRET_KEYS || HAS_INVALID_SECRET_KEYS} - successMsg={secretKeySuccessMsg} - errorMsg={secretKeyErrorMsg} - /> - - - - - { - updateBipPath(e.target.value); - - const error = validate.getBipPathError(e.target.value); - - if (error) { - setBipPathErrorMsg(error); - } else { - setBipPathErrorMsg(""); - } - }} - error={bipPathErrorMsg} - value={sign.bipPath} - note={ - selectedHardware === "trezor" - ? "Note: Trezor devices require upper time bounds to be set (non-zero), otherwise the signature will not be verified" - : undefined - } - rightElement={ - <> -
- -
- - } - /> -
- - { - handleSign({ - sigType: "hardwareWallet", - isClear: false, - }); - }} - onClear={() => { - handleSign({ - sigType: "hardwareWallet", - isClear: true, - }); - }} - isLoading={isHardwareLoading} - isDisabled={!selectedHardware || !sign.bipPath} - successMsg={hardwareSuccessMsg} - errorMsg={hardwareErrorMsg} - /> -
- - - Sign with wallet extension - - { - setIsExtensionClear(false); - setIsExtensionLoading(true); - - trackEvent(TrackingEvent.TRANSACTION_SIGN_WALLET); - }} - onClear={() => { - setIsExtensionClear(true); - handleSign({ sigType: "extensionWallet", isClear: true }); - }} - isLoading={isExtensionLoading} - successMsg={exSuccessMsg} - errorMsg={exErrorMsg} - /> - - - - Add a signature - - <> - {sigInputs.map((_, idx) => ( - - - handleSignatureOnChange( - e.target.value, - "publicKey", - idx, - ) - } - /> - - - handleSignatureOnChange( - e.target.value, - "signature", - idx, - ) - } - autocomplete="off" - /> - - <> - {idx !== 0 ? ( - - - - ) : null} - - - ))} -
- Use this section to add signature(s) to the transaction - envelope -
- - - -
-
-
- - {sign.signedTx ? ( -
- -
-
- {sign.signedTx} -
-
- - } - note={ - <> - Now that this transaction is signed, you can submit it to the - network. Horizon provides an endpoint called Post Transaction - that will relay your transaction to the network and inform you - of the result. - - } - footerLeftEl={ - - - - - - } - footerRightEl={ -
- { - trackEvent(TrackingEvent.TRANSACTION_SIGN_VIEW_IN_XDR); - }} - /> - - -
- } - /> -
- ) : null} - - {signError ? ( - - ) : null} -
- - ); -}; diff --git a/src/app/(sidebar)/transaction/sign/page.tsx b/src/app/(sidebar)/transaction/sign/page.tsx deleted file mode 100644 index 749288427..000000000 --- a/src/app/(sidebar)/transaction/sign/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import { useStore } from "@/store/useStore"; - -import { Import } from "./components/Import"; -import { Overview } from "./components/Overview"; - -export default function SignTransaction() { - const { transaction } = useStore(); - const { sign } = transaction; - - return ( -
- {sign.activeView === "overview" ? : } -
- ); -} diff --git a/src/app/page.tsx b/src/app/page.tsx index c65339fc4..d56bd5aac 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -57,8 +57,8 @@ export default function Introduction() { build ,{" "} - - sign + + import ,{" "} ({ const rowKey = `${tableId}-row-${rowIdx}`; return ( - + {row.map((cell, cellIdx) => ( ({ data-disabled={isUpdating || isExternalUpdating} > - + {tableHeaders.map((th, idx) => (
diff --git a/src/constants/navItems.tsx b/src/constants/navItems.tsx index ece6f9f4b..a547ebaee 100644 --- a/src/constants/navItems.tsx +++ b/src/constants/navItems.tsx @@ -110,10 +110,6 @@ const TRANSACTION_NAV = [ route: Routes.IMPORT_TRANSACTION, label: "Import transaction", }, - { - route: Routes.SIGN_TRANSACTION, - label: "Sign transaction", - }, { route: Routes.FEE_BUMP_TRANSACTION, label: "Fee bump", diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 3e50fc298..650d63fa7 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -81,7 +81,6 @@ export enum Routes { TRANSACTION = "/transaction", BUILD_TRANSACTION = "/transaction/build", IMPORT_TRANSACTION = "/transaction/import", - SIGN_TRANSACTION = "/transaction/sign", CLI_SIGN_TRANSACTION = "/transaction/cli-sign", SIMULATE_TRANSACTION = "/transaction/simulate", SUBMIT_TRANSACTION = "/transaction/submit", diff --git a/tests/e2e/feeBumpPage.test.ts b/tests/e2e/feeBumpPage.test.ts index 07a0f6834..178fced19 100644 --- a/tests/e2e/feeBumpPage.test.ts +++ b/tests/e2e/feeBumpPage.test.ts @@ -60,7 +60,7 @@ test.describe("Fee Bump Page", () => { await expect(nextButton).toBeVisible(); await nextButton.click(); - await expect(page.locator("h1")).toHaveText("Sign transaction"); + await expect(page.locator("h1")).toHaveText("Import transaction"); }); test("View in XDR viewer", async ({ page }) => { diff --git a/tests/e2e/importTransactionPage.test.ts b/tests/e2e/importTransactionPage.test.ts new file mode 100644 index 000000000..15a49aa54 --- /dev/null +++ b/tests/e2e/importTransactionPage.test.ts @@ -0,0 +1,227 @@ +import { baseURL } from "../../playwright.config"; +import { test, expect } from "@playwright/test"; + +test.describe("Import Transaction Page", () => { + test.beforeEach(async ({ page }) => { + await page.goto(`${baseURL}/transaction/import`); + }); + + test("Loads", async ({ page }) => { + await expect(page.locator("h1")).toHaveText("Import transaction"); + }); + + test("Imports a VALID Classic Transaction and shows the overview", async ({ + page, + }) => { + const xdrInput = page.getByLabel("Transaction envelope in XDR"); + await xdrInput.fill(MOCK_TX_XDR); + + // Once the XDR parses, the input is replaced by a success alert and the + // overview card appears. + await expect( + page.getByText("Transaction imported successfully."), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Transaction overview" }), + ).toBeVisible(); + + /*** TX Overview Details ***/ + // Network passphrase + await expect(page.getByLabel("Signing for")).toHaveValue( + "Test SDF Network ; September 2015", + ); + + // TX XDR + await expect(page.getByLabel("Transaction envelope XDR")).toHaveValue( + MOCK_TX_XDR, + ); + + // TX HASH + await expect(page.getByLabel("Transaction hash")).toHaveValue( + "794e2073e130dc09d2b7e8b147b51f6ef75ff171c83c603bc8ab4cffa3f341a1", + ); + + // Source Account + await expect(page.getByLabel("Source account")).toHaveValue( + "GDE25LQ34AFCSDMYTOI6AVVEHRXFRJI4MOAVIUGUDUQEC5ZWN5OZDLAZ", + ); + + // Sequence number + await expect(page.getByLabel("Sequence number")).toHaveValue( + "2345052143617", + ); + + // Transaction Fee (stroops) + await expect(page.getByLabel("Transaction fee (stroops)")).toHaveValue( + "100", + ); + + // Number of operations + await expect(page.getByLabel("Number of operations")).toHaveValue("1"); + + // This envelope carries no signatures, so the Signatures section is hidden. + await expect(page.getByText("Signer", { exact: true })).toBeHidden(); + + // The Next button defaults to signing for an unsigned classic tx. + const nextButton = page.locator('[data-position="right"]'); + await expect(nextButton).toHaveText("Sign transaction"); + await expect(nextButton).toBeEnabled(); + }); + + test("Imports a VALID Soroban Transaction and shows the overview", async ({ + page, + }) => { + const xdrInput = page.getByLabel("Transaction envelope in XDR"); + await xdrInput.fill(MOCK_SOROBAN_XDR); + + await expect( + page.getByText("Transaction imported successfully."), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Transaction overview" }), + ).toBeVisible(); + + /*** TX Overview Details ***/ + // Network passphrase + await expect(page.getByLabel("Signing for")).toHaveValue( + "Test SDF Network ; September 2015", + ); + + // TX XDR + await expect(page.getByLabel("Transaction envelope XDR")).toHaveValue( + MOCK_SOROBAN_XDR, + ); + + // TX HASH + await expect(page.getByLabel("Transaction hash")).toHaveValue( + "ab12155abb53a5f8177e47683a870976621fcac62cc3441d997cc0aafaac99ad", + ); + + // Source Account + await expect(page.getByLabel("Source account")).toHaveValue( + "GBOTV3EYB4BO26MK3PFXNDWKI54XGXMLMK52F7TYLNOOQLL2GCJGBUQQ", + ); + + // Sequence number + await expect(page.getByLabel("Sequence number")).toHaveValue( + "1112100176920589", + ); + + // Transaction Fee (stroops) + await expect(page.getByLabel("Transaction fee (stroops)")).toHaveValue( + "1920974", + ); + + // Number of operations + await expect(page.getByLabel("Number of operations")).toHaveValue("1"); + + // An unsigned Soroban tx must be simulated before signing, so the Next + // button defaults to the simulate step. + const nextButton = page.locator('[data-position="right"]'); + await expect(nextButton).toHaveText("Simulate transaction"); + await expect(nextButton).toBeEnabled(); + }); + + test("Imports a transaction with 3 operations", async ({ page }) => { + const xdrInput = page.getByLabel("Transaction envelope in XDR"); + await xdrInput.fill(MOCK_TX_XDR_3_OPERATIONS); + + await expect( + page.getByRole("heading", { name: "Transaction overview" }), + ).toBeVisible(); + + /*** TX Overview Details ***/ + // Network passphrase + await expect(page.getByLabel("Signing for")).toHaveValue( + "Test SDF Network ; September 2015", + ); + + // TX XDR + await expect(page.getByLabel("Transaction envelope XDR")).toHaveValue( + MOCK_TX_XDR_3_OPERATIONS, + ); + + // TX HASH + await expect(page.getByLabel("Transaction hash")).toHaveValue( + "892110aecc9f30662d5ececcf2a1f2fdd03fc42f3b3ca55c475a05d421838e60", + ); + + // Source Account + await expect(page.getByLabel("Source account")).toHaveValue( + "GBTQEP2NS6WSRRXYXZ4JJLLLO4OWH5LWHZFEGL5PMOQQDELD4MY5YUWJ", + ); + + // Sequence number + await expect(page.getByLabel("Sequence number")).toHaveValue( + "4552819952582657", + ); + + // Transaction Fee (stroops) + await expect(page.getByLabel("Transaction fee (stroops)")).toHaveValue( + "300", + ); + + // Number of operations + await expect(page.getByLabel("Number of operations")).toHaveValue("3"); + }); + + test("Advances to the sign step", async ({ page }) => { + const xdrInput = page.getByLabel("Transaction envelope in XDR"); + await xdrInput.fill(MOCK_TX_XDR); + + const nextButton = page.locator('[data-position="right"]'); + await expect(nextButton).toHaveText("Sign transaction"); + await nextButton.click(); + + await expect(page.locator("h1")).toHaveText("Sign transaction"); + await expect( + page.getByText( + "To be included in the ledger, the transaction must be signed and submitted to the network.", + ), + ).toBeVisible(); + }); + + test("Shows an error for an ScVal Type XDR that is not a Transaction Envelope", async ({ + page, + }) => { + const decodeErrorMsg = page.getByText( + "Unable to parse input XDR into Transaction Envelope", + ); + + await expect(decodeErrorMsg).toBeHidden(); + + const xdrInput = page.getByLabel("Transaction envelope in XDR"); + await xdrInput.fill(MOCK_SC_VAL_XDR); + + await expect(decodeErrorMsg).toBeVisible(); + }); + + test("Shows an error for invalid XDR", async ({ page }) => { + const decodeErrorMsg = page.getByText( + "Unable to parse input XDR into Transaction Envelope", + ); + + await expect(decodeErrorMsg).toBeHidden(); + + const xdrInput = page.getByLabel("Transaction envelope in XDR"); + await xdrInput.fill("AAA"); + + await expect(decodeErrorMsg).toBeVisible(); + }); +}); + +// ============================================================================= +// Mock data +// ============================================================================= + +const MOCK_SC_VAL_XDR = + "AAAAEQAAAAEAAAAGAAAADwAAAAZhbW91bnQAAAAAAAoAAAAAAAAAAAAACRhOcqAAAAAADwAAAAxib290c3RyYXBwZXIAAAASAAAAARssFqxD/prgmYc9vGkaqslWrGlPINzMYTLc4yqRfO3AAAAADwAAAAxjbG9zZV9sZWRnZXIAAAADAz6ilAAAAA8AAAAIcGFpcl9taW4AAAAKAAAAAAAAAAAAAAARdlkuAAAAAA8AAAAEcG9vbAAAABIAAAABX/a7xfliM8nFgGel6pbCM6fT/kqrHAITNtWZQXgDlIIAAAAPAAAAC3Rva2VuX2luZGV4AAAAAAMAAAAA"; + +const MOCK_TX_XDR = + "AAAAAgAAAADJrq4b4AopDZibkeBWpDxuWKUcY4FUUNQdIEF3Nm9dkQAAAGQAAAIiAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACXlGN76T6NQcaUJxbEkH3mi1HHWsHnLqMDdlLl9NlJgQAAAAAAAAAABfXhAAAAAAAAAAAA"; + +const MOCK_TX_XDR_3_OPERATIONS = + "AAAAAgAAAABnAj9Nl60oxvi+eJSta3cdY/V2PkpDL69joQGRY+Mx3AAAASwAECzEAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAEAAAAABHLXc6lPRFz7BJua75KzEQi1Iw3Hj6bUXLrNdMRPZmYwAAAAAAAAAAAAAAAEctdzqU9EXPsEm5rvkrMRCLUjDcePptRcus10xE9mZjAAAAAAExLQAAAAAAAAAAEQAAAAAAAAAA"; + +const MOCK_SOROBAN_XDR = + "AAAAAgAAAABdOuyYDwLteYrby3aOykd5c12LYrui/nhbXOgtejCSYAAdT84AA/NzAAAADQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABlL5dkzzoyLMbmYEsrdy39HekdojWLuKyg7lC02jClEoAAAAEbWludAAAAAIAAAASAAAAAAAAAADRh2IaEYIegGIrvdxKRF5FM9AGClfaqPnxY7SmWduVggAAAAoAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAZS+XZM86MizG5mBLK3ct/R3pHaI1i7isoO5QtNowpRKAAAABG1pbnQAAAACAAAAEgAAAAAAAAAA0YdiGhGCHoBiK73cSkReRTPQBgpX2qj58WO0plnblYIAAAAKAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAHJ9d4ncm4GZ+WLWTVqsnHAEkBZcuJ/PoXwvzFTQBx5ygAAAACAAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAADRh2IaEYIegGIrvdxKRF5FM9AGClfaqPnxY7SmWduVggAAAAEAAAAGAAAAAZS+XZM86MizG5mBLK3ct/R3pHaI1i7isoO5QtNowpRKAAAAFAAAAAEAL4fdAABS/AAAAfwAAAAAAB1PBgAAAAA="; diff --git a/tests/e2e/signStepContent.test.ts b/tests/e2e/signStepContent.test.ts index 21f874436..443b18bb6 100644 --- a/tests/e2e/signStepContent.test.ts +++ b/tests/e2e/signStepContent.test.ts @@ -74,7 +74,7 @@ test.describe("Sign Step in Build Flow", () => { }) => { await seedSessionStorageAndNavigate(page); - await expect(page.locator("h1")).toHaveText("Sign transaction"); + await expect(page.locator("h1")).toHaveText("Import transaction"); await expect( page.getByText( "To be included in the ledger, the transaction must be signed and submitted to the network.", @@ -100,7 +100,7 @@ test.describe("Sign Step in Build Flow", () => { await seedSessionStorageAndNavigate(page); // Wait for the sign step to hydrate from sessionStorage - await expect(page.locator("h1")).toHaveText("Sign transaction"); + await expect(page.locator("h1")).toHaveText("Import transaction"); const nextButton = page.locator('[data-position="right"]'); await expect(nextButton).toBeDisabled(); diff --git a/tests/e2e/signTransactionPage.test.ts b/tests/e2e/signTransactionPage.test.ts deleted file mode 100644 index 32d01ceda..000000000 --- a/tests/e2e/signTransactionPage.test.ts +++ /dev/null @@ -1,435 +0,0 @@ -import { baseURL } from "../../playwright.config"; -import { test, expect } from "@playwright/test"; - -test.describe("Sign Transaction Page", () => { - test.beforeEach(async ({ page }) => { - await page.goto(`${baseURL}/transaction/sign`); - }); - - test("Loads", async ({ page }) => { - await expect(page.locator("h1")).toHaveText("Sign transaction"); - }); - - test("Overview with a VALID Classic Transaction with ONE operation envelope XDR", async ({ - page, - }) => { - // sections - const overview = page.getByTestId("sign-tx-overview"); - const signaturesView = page.getByTestId("sign-tx-sigs"); - const secretKeysView = page.getByTestId("sign-tx-secretkeys"); - const hardwareView = page.getByTestId("sign-tx-hardware"); - const walletExtView = page.getByTestId("sign-tx-wallet-ext"); - const sigExtView = page.getByTestId("sign-tx-signature"); - const validationView = page.getByTestId("sign-tx-validation-card"); - const signedXdr = page.getByTestId("validation-card-response"); - - // Import Screen - const importBtn = page.getByRole("button", { name: "Import transaction" }); - const validMsg = page.getByText("Valid Transaction Envelope XDR"); - - const xdrInput = page.getByLabel( - "Import a transaction envelope in XDR format", - ); - await xdrInput.fill(MOCK_TX_XDR); - - await expect(validMsg).toBeVisible(); - - await importBtn.click(); - - // Overview and Signatures Screen - await expect(overview).toBeVisible(); - await expect(signaturesView).toBeVisible(); - - /*** TX Overview Details ***/ - // Network passphrase - const overviewSigning = page.getByLabel("Signing for"); - await expect(overviewSigning).toHaveValue( - "Test SDF Network ; September 2015", - ); - - // TX XDR - const overviewTxXDR = page.getByLabel("Transaction Envelope XDR"); - await expect(overviewTxXDR).toHaveValue(MOCK_TX_XDR); - - // TX HASH - const overviewTxHash = page.getByLabel("Transaction hash"); - await expect(overviewTxHash).toHaveValue( - "794e2073e130dc09d2b7e8b147b51f6ef75ff171c83c603bc8ab4cffa3f341a1", - ); - - // Source Account - const overviewSource = page.getByLabel("Source account"); - await expect(overviewSource).toHaveValue( - "GDE25LQ34AFCSDMYTOI6AVVEHRXFRJI4MOAVIUGUDUQEC5ZWN5OZDLAZ", - ); - - // Sequence number - const overviewSeq = page.getByLabel("Sequence number"); - await expect(overviewSeq).toHaveValue("2345052143617"); - - // Transaction Fee (stroops) - const overviewTxFee = page.getByLabel("Transaction Fee (stroops)"); - await expect(overviewTxFee).toHaveValue("100"); - - // Number of operations - const overviewOpsNum = page.getByLabel("Number of operations"); - await expect(overviewOpsNum).toHaveValue("1"); - - /*** Signatures: Secret Keys ***/ - // Click 'Add additional' button to get an additional secret input field - const multiPickerContainer = page.getByTestId("multipicker-signer"); - const multiPickerInput = multiPickerContainer.getByPlaceholder( - "Secret key (starting with S) or hash preimage (in hex)", - ); - const addAddlSecretBtn = multiPickerContainer.getByText("Add additional"); - const secretKeysSignBtn = secretKeysView.getByText("Sign transaction"); - const invalidSecretKeyErrorMsg = page.getByText( - "Invalid secret key. Please check your secret key and try again.", - ); - const successSecretKeyMsg = secretKeysView.getByText( - "Successfully added 1 signature", - ); - - await expect(multiPickerInput).toHaveCount(1); - await addAddlSecretBtn.click(); - await expect(multiPickerInput).toHaveCount(2); - - // Type in a string in an invalid secret key format - await multiPickerInput.nth(0).fill("lkjlsdkjflksdjf"); - await expect(invalidSecretKeyErrorMsg).toBeVisible(); - - // Type in a string in an valid secret key format - await multiPickerInput - .nth(0) - .fill("SCAM6CZNCLJFQOGSC7LLE2KMBYCBD7S5IYV447MZX5NHPGCHRHPYITCF"); - await expect(invalidSecretKeyErrorMsg).toBeHidden(); - await expect(secretKeysSignBtn).toBeEnabled(); - - await secretKeysSignBtn.click(); - - await expect(successSecretKeyMsg).toBeVisible(); - - const firstSignedResponse = signedXdr.getByText( - "AAAAAgAAAADJrq4b4AopDZibkeBWpDxuWKUcY4FUUNQdIEF3Nm9dkQAAAGQAAAIiAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACXlGN76T6NQcaUJxbEkH3mi1HHWsHnLqMDdlLl9NlJgQAAAAAAAAAABfXhAAAAAAAAAAABjc9OtQAAAEBrpI8Q90yqEqjcLSubVj5nqtyt53bpVzi8Bzikps4xuom0xHQgrM6MsQS503ortwLcYOw0gyLPyst7J88ZDoQJ", - ); - - await expect(firstSignedResponse).toBeVisible(); - - /*** Signatures: Hardware Wallet ***/ - const bipPathInput = hardwareView.getByPlaceholder( - "BIP path in format: 44'/148'/0'", - ); - - await expect(bipPathInput).toHaveValue("44'/148'/0'"); - - const hardwareSelect = hardwareView.locator("id=hardware-wallet-select"); - - await hardwareSelect.click(); - - const options = await hardwareSelect.locator("option").allTextContents(); - expect(options).toEqual([ - "Select a wallet", - "Ledger", - "Hash with Ledger", - "Trezor", - ]); - - const hardwareSignBtn = hardwareView.getByText("Sign transaction"); - - await hardwareSignBtn.isDisabled(); - - await hardwareSelect.selectOption("Trezor"); - - await expect(hardwareSelect).toHaveValue("trezor"); - - await hardwareSignBtn.isEnabled(); - - await hardwareSelect.selectOption("Ledger"); - - await expect(hardwareSelect).toHaveValue("ledger"); - - await hardwareSelect.selectOption("Hash with Ledger"); - - await expect(hardwareSelect).toHaveValue("ledger_hash"); - - /*** Signatures: Wallet Extension ***/ - const walletExtSignBtn = walletExtView.getByRole("button", { - name: "Sign with wallet", - }); - await walletExtSignBtn.click(); - - await expect( - page.getByRole("heading", { name: "Connect Wallet" }), - ).toBeVisible(); - - const connectWalletModal = page.locator(".stellar-wallets-kit").filter({ - has: page.getByRole("heading", { name: "Connect Wallet" }), - }); - - // Wallet Extension to display 8 wallets - await expect(connectWalletModal.getByRole("listitem")).toHaveCount(8); - - // Exit out of the wallet extension modal - await page.click("body", { position: { x: 10, y: 10 } }); - - /*** Signatures: Add a signature ***/ - const addSigBtn = sigExtView.getByText("Add signature to transaction"); - await addSigBtn.isDisabled(); - - const pubKeyInput = sigExtView.getByPlaceholder("Public key"); - const sigInput = sigExtView.getByPlaceholder( - "Hex encoded 64-byte ed25519 signature", - ); - const pubKeyErrorMsg = sigExtView.getByText("Public key is invalid."); - const addSigErrorMsg = sigExtView.getByText( - "Error: invalid encoded string", - ); - const addSigSuccessMsg = sigExtView.getByText( - "Successfully added 1 signature", - ); - - await expect(pubKeyInput).toBeVisible(); - await expect(sigInput).toBeVisible(); - - pubKeyInput.fill("sdfsdf"); - - await expect(pubKeyErrorMsg).toBeVisible(); - - sigInput.fill( - "ef6db30947dafea9f87f821751812dc15180f084c70dfab6e359bc92fa892f10aa0eb403c37ccc77c67cb0fabc77eba6e151485a72c5e549c58a2f57f0c26101", - ); - - await addSigBtn.isEnabled(); - await addSigBtn.click(); - - await expect(addSigErrorMsg).toBeVisible(); - - pubKeyInput.fill( - "GDBE5AQAPXR6DYK7WPTWY25KM4TN552VZ3543DZUGUND7KI2TB2SIAJX", - ); - - await expect(addSigErrorMsg).toBeHidden(); - - await addSigBtn.click(); - - await expect(addSigSuccessMsg).toBeVisible(); - - await expect(signedXdr).toContainText( - "AAAAAgAAAADJrq4b4AopDZibkeBWpDxuWKUcY4FUUNQdIEF3Nm9dkQAAAGQAAAIiAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACXlGN76T6NQcaUJxbEkH3mi1HHWsHnLqMDdlLl9NlJgQAAAAAAAAAABfXhAAAAAAAAAAACjc9OtQAAAEBrpI8Q90yqEqjcLSubVj5nqtyt53bpVzi8Bzikps4xuom0xHQgrM6MsQS503ortwLcYOw0gyLPyst7J88ZDoQJGph1JAAAAEDvbbMJR9r+qfh/ghdRgS3BUYDwhMcN+rbjWbyS+okvEKoOtAPDfMx3xnyw+rx366bhUUhacsXlScWKL1fwwmEB", - ); - - const submitBtn = validationView.getByText("Submit transaction"); - const simulateBtn = validationView.getByText("Simulate transaction"); - - await expect(submitBtn).toBeEnabled(); - await expect(simulateBtn).toBeDisabled(); - - await submitBtn.click(); - }); - - test("Overview with a VALID Soroban Transaction", async ({ page }) => { - // sections - const overview = page.getByTestId("sign-tx-overview"); - const signaturesView = page.getByTestId("sign-tx-sigs"); - const secretKeysView = page.getByTestId("sign-tx-secretkeys"); - const validationView = page.getByTestId("sign-tx-validation-card"); - const signedXdr = page.getByTestId("validation-card-response"); - - // Import Screen - const importBtn = page.getByRole("button", { name: "Import transaction" }); - const validMsg = page.getByText("Valid Transaction Envelope XDR"); - - const xdrInput = page.getByLabel( - "Import a transaction envelope in XDR format", - ); - await xdrInput.fill(MOCK_SOROBAN_XDR); - - await expect(validMsg).toBeVisible(); - - await importBtn.click(); - - // Overview and Signatures Screen - await expect(overview).toBeVisible(); - await expect(signaturesView).toBeVisible(); - - /*** TX Overview Details ***/ - // Network passphrase - const overviewSigning = page.getByLabel("Signing for"); - await expect(overviewSigning).toHaveValue( - "Test SDF Network ; September 2015", - ); - - // TX XDR - const overviewTxXDR = page.getByLabel("Transaction Envelope XDR"); - await expect(overviewTxXDR).toHaveValue(MOCK_SOROBAN_XDR); - - // TX HASH - const overviewTxHash = page.getByLabel("Transaction hash"); - await expect(overviewTxHash).toHaveValue( - "ab12155abb53a5f8177e47683a870976621fcac62cc3441d997cc0aafaac99ad", - ); - - // Source Account - const overviewSource = page.getByLabel("Source account"); - await expect(overviewSource).toHaveValue( - "GBOTV3EYB4BO26MK3PFXNDWKI54XGXMLMK52F7TYLNOOQLL2GCJGBUQQ", - ); - - // Sequence number - const overviewSeq = page.getByLabel("Sequence number"); - await expect(overviewSeq).toHaveValue("1112100176920589"); - - // Transaction Fee (stroops) - const overviewTxFee = page.getByLabel("Transaction Fee (stroops)"); - await expect(overviewTxFee).toHaveValue("1920974"); - - // Number of operations - const overviewOpsNum = page.getByLabel("Number of operations"); - await expect(overviewOpsNum).toHaveValue("1"); - - /*** Signatures: Secret Keys ***/ - // Click 'Add additional' button to get an additional secret input field - const multiPickerContainer = page.getByTestId("multipicker-signer"); - const multiPickerInput = multiPickerContainer.getByPlaceholder( - "Secret key (starting with S) or hash preimage (in hex)", - ); - const secretKeysSignBtn = secretKeysView.getByText("Sign transaction"); - const invalidSecretKeyErrorMsg = page.getByText( - "Invalid secret key. Please check your secret key and try again.", - ); - const successSecretKeyMsg = secretKeysView.getByText( - "Successfully added 1 signature", - ); - - await expect(multiPickerInput).toHaveCount(1); - - // Type in a string in an valid secret key format - await multiPickerInput - .nth(0) - .fill("SCX5AFVSAW7NUDWMYIO6E5BVLNHHU4YELSP2YI66YAQKPQXT2UXJWLJ6"); - await expect(invalidSecretKeyErrorMsg).toBeHidden(); - await expect(secretKeysSignBtn).toBeEnabled(); - - await secretKeysSignBtn.click(); - - await expect(successSecretKeyMsg).toBeVisible(); - - await expect(signedXdr).toContainText( - "AAAAAgAAAABdOuyYDwLteYrby3aOykd5c12LYrui/nhbXOgtejCSYAAdT84AA/NzAAAADQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABlL5dkzzoyLMbmYEsrdy39HekdojWLuKyg7lC02jClEoAAAAEbWludAAAAAIAAAASAAAAAAAAAADRh2IaEYIegGIrvdxKRF5FM9AGClfaqPnxY7SmWduVggAAAAoAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAZS+XZM86MizG5mBLK3ct/R3pHaI1i7isoO5QtNowpRKAAAABG1pbnQAAAACAAAAEgAAAAAAAAAA0YdiGhGCHoBiK73cSkReRTPQBgpX2qj58WO0plnblYIAAAAKAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAHJ9d4ncm4GZ+WLWTVqsnHAEkBZcuJ/PoXwvzFTQBx5ygAAAACAAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAADRh2IaEYIegGIrvdxKRF5FM9AGClfaqPnxY7SmWduVggAAAAEAAAAGAAAAAZS+XZM86MizG5mBLK3ct/R3pHaI1i7isoO5QtNowpRKAAAAFAAAAAEAL4fdAABS/AAAAfwAAAAAAB1PBgAAAAF6MJJgAAAAQA+i1MPVxoGS9hO+roDg4od2KV1TmaaeuTwswZ5FLW/24F+oRFFwUetyxQ8SKfY0SPSyCvJdVo1bbv/65NugBwI=", - ); - - const submitBtn = validationView.getByText("Submit transaction"); - const simulateBtn = validationView.getByText("Simulate transaction"); - - await expect(submitBtn).toBeEnabled(); - await expect(simulateBtn).toBeEnabled(); - - await submitBtn.click(); - }); - - test("Use an ScVal Type XDR that is not Transaction Envelope XDR", async ({ - page, - }) => { - const decodeErrorMsg = page.getByText( - "Unable to parse input XDR into Transaction Envelope", - ); - - await expect(decodeErrorMsg).toBeHidden(); - - const xdrInput = page.getByLabel( - "Import a transaction envelope in XDR format", - ); - await xdrInput.fill(MOCK_SC_VAL_XDR); - - await expect(decodeErrorMsg).toBeVisible(); - }); - - test("Use an XDR that includes 3 operations", async ({ page }) => { - // sections - const overview = page.getByTestId("sign-tx-overview"); - const signaturesView = page.getByTestId("sign-tx-sigs"); - - // Import Screen - const importBtn = page.getByRole("button", { name: "Import transaction" }); - const validMsg = page.getByText("Valid Transaction Envelope XDR"); - - const xdrInput = page.getByLabel( - "Import a transaction envelope in XDR format", - ); - await xdrInput.fill(MOCK_TX_XDR_3_OPERATIONS); - - await expect(validMsg).toBeVisible(); - - await importBtn.click(); - - // Overview and Signatures Screen - await expect(overview).toBeVisible(); - await expect(signaturesView).toBeVisible(); - - /*** TX Overview Details ***/ - // Network passphrase - const overviewSigning = page.getByLabel("Signing for"); - await expect(overviewSigning).toHaveValue( - "Test SDF Network ; September 2015", - ); - - // TX XDR - const overviewTxXDR = page.getByLabel("Transaction Envelope XDR"); - await expect(overviewTxXDR).toHaveValue(MOCK_TX_XDR_3_OPERATIONS); - - // TX HASH - const overviewTxHash = page.getByLabel("Transaction hash"); - await expect(overviewTxHash).toHaveValue( - "892110aecc9f30662d5ececcf2a1f2fdd03fc42f3b3ca55c475a05d421838e60", - ); - - // Source Account - const overviewSource = page.getByLabel("Source account"); - await expect(overviewSource).toHaveValue( - "GBTQEP2NS6WSRRXYXZ4JJLLLO4OWH5LWHZFEGL5PMOQQDELD4MY5YUWJ", - ); - - // Sequence number - const overviewSeq = page.getByLabel("Sequence number"); - await expect(overviewSeq).toHaveValue("4552819952582657"); - - // Transaction Fee (stroops) - const overviewTxFee = page.getByLabel("Transaction Fee (stroops)"); - await expect(overviewTxFee).toHaveValue("300"); - - // Number of operations - const overviewOpsNum = page.getByLabel("Number of operations"); - await expect(overviewOpsNum).toHaveValue("3"); - }); - - test("Invalid XDR", async ({ page }) => { - const decodeErrorMsg = page.getByText( - "Unable to parse input XDR into Transaction Envelope", - ); - - await expect(decodeErrorMsg).toBeHidden(); - - const xdrInput = page.getByLabel( - "Import a transaction envelope in XDR format", - ); - await xdrInput.fill("AAA"); - - await expect(decodeErrorMsg).toBeVisible(); - }); -}); - -// ============================================================================= -// Mock data -// ============================================================================= - -const MOCK_SC_VAL_XDR = - "AAAAEQAAAAEAAAAGAAAADwAAAAZhbW91bnQAAAAAAAoAAAAAAAAAAAAACRhOcqAAAAAADwAAAAxib290c3RyYXBwZXIAAAASAAAAARssFqxD/prgmYc9vGkaqslWrGlPINzMYTLc4yqRfO3AAAAADwAAAAxjbG9zZV9sZWRnZXIAAAADAz6ilAAAAA8AAAAIcGFpcl9taW4AAAAKAAAAAAAAAAAAAAARdlkuAAAAAA8AAAAEcG9vbAAAABIAAAABX/a7xfliM8nFgGel6pbCM6fT/kqrHAITNtWZQXgDlIIAAAAPAAAAC3Rva2VuX2luZGV4AAAAAAMAAAAA"; - -const MOCK_TX_XDR = - "AAAAAgAAAADJrq4b4AopDZibkeBWpDxuWKUcY4FUUNQdIEF3Nm9dkQAAAGQAAAIiAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACXlGN76T6NQcaUJxbEkH3mi1HHWsHnLqMDdlLl9NlJgQAAAAAAAAAABfXhAAAAAAAAAAAA"; - -const MOCK_TX_XDR_3_OPERATIONS = - "AAAAAgAAAABnAj9Nl60oxvi+eJSta3cdY/V2PkpDL69joQGRY+Mx3AAAASwAECzEAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAEAAAAABHLXc6lPRFz7BJua75KzEQi1Iw3Hj6bUXLrNdMRPZmYwAAAAAAAAAAAAAAAEctdzqU9EXPsEm5rvkrMRCLUjDcePptRcus10xE9mZjAAAAAAExLQAAAAAAAAAAEQAAAAAAAAAA"; - -const MOCK_SOROBAN_XDR = - "AAAAAgAAAABdOuyYDwLteYrby3aOykd5c12LYrui/nhbXOgtejCSYAAdT84AA/NzAAAADQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABlL5dkzzoyLMbmYEsrdy39HekdojWLuKyg7lC02jClEoAAAAEbWludAAAAAIAAAASAAAAAAAAAADRh2IaEYIegGIrvdxKRF5FM9AGClfaqPnxY7SmWduVggAAAAoAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAZS+XZM86MizG5mBLK3ct/R3pHaI1i7isoO5QtNowpRKAAAABG1pbnQAAAACAAAAEgAAAAAAAAAA0YdiGhGCHoBiK73cSkReRTPQBgpX2qj58WO0plnblYIAAAAKAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAAHJ9d4ncm4GZ+WLWTVqsnHAEkBZcuJ/PoXwvzFTQBx5ygAAAACAAAABgAAAAGUvl2TPOjIsxuZgSyt3Lf0d6R2iNYu4rKDuULTaMKUSgAAABAAAAABAAAAAgAAAA8AAAAHQmFsYW5jZQAAAAASAAAAAAAAAADRh2IaEYIegGIrvdxKRF5FM9AGClfaqPnxY7SmWduVggAAAAEAAAAGAAAAAZS+XZM86MizG5mBLK3ct/R3pHaI1i7isoO5QtNowpRKAAAAFAAAAAEAL4fdAABS/AAAAfwAAAAAAB1PBgAAAAA="; diff --git a/tests/e2e/signerSelector.test.ts b/tests/e2e/signerSelector.test.ts index b047ee3c7..4373797d1 100644 --- a/tests/e2e/signerSelector.test.ts +++ b/tests/e2e/signerSelector.test.ts @@ -87,7 +87,7 @@ test.describe("Signer Selector", () => { }); test("Loads", async () => { - await expect(pageContext.locator("h1")).toHaveText("Sign transaction"); + await expect(pageContext.locator("h1")).toHaveText("Import transaction"); }); test("'Use secret key' dropdown works for source account", async () => { From 8225658b8d67457adeafd6168fecffe22ff00383 Mon Sep 17 00:00:00 2001 From: Jeesun Kim Date: Thu, 11 Jun 2026 13:54:53 -0700 Subject: [PATCH 2/6] update 'cli-sign' to use import transaction --- src/app/(sidebar)/transaction/cli-sign/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(sidebar)/transaction/cli-sign/page.tsx b/src/app/(sidebar)/transaction/cli-sign/page.tsx index 1f8a6edb7..578b5c93c 100644 --- a/src/app/(sidebar)/transaction/cli-sign/page.tsx +++ b/src/app/(sidebar)/transaction/cli-sign/page.tsx @@ -38,7 +38,7 @@ export default function CliSignTransaction() { trackEvent(TrackingEvent.TRANSACTION_CLI_SIGN); - router.push(Routes.SIGN_TRANSACTION); + router.push(Routes.IMPORT_TRANSACTION); // Not including other deps // eslint-disable-next-line react-hooks/exhaustive-deps }, [transaction.sign.importXdr, network.id]); From 25e08bbe4b470458f3d9a04f0c16805711b372f3 Mon Sep 17 00:00:00 2001 From: Jeesun Kim Date: Thu, 11 Jun 2026 14:20:17 -0700 Subject: [PATCH 3/6] fix page --- .../(sidebar)/transaction/cli-sign/page.tsx | 38 +++++--- .../import/components/ImportStepContent.tsx | 81 +++++++---------- src/helpers/parseImportXdr.ts | 88 +++++++++++++++++++ 3 files changed, 146 insertions(+), 61 deletions(-) create mode 100644 src/helpers/parseImportXdr.ts diff --git a/src/app/(sidebar)/transaction/cli-sign/page.tsx b/src/app/(sidebar)/transaction/cli-sign/page.tsx index 578b5c93c..704a68d25 100644 --- a/src/app/(sidebar)/transaction/cli-sign/page.tsx +++ b/src/app/(sidebar)/transaction/cli-sign/page.tsx @@ -8,17 +8,26 @@ import { Routes } from "@/constants/routes"; import { NetworkOptions } from "@/constants/settings"; import { useStore } from "@/store/useStore"; import { trackEvent, TrackingEvent } from "@/metrics/tracking"; - +import { useImportFlowStore } from "@/store/createTransactionFlowStore"; +import { delayedAction } from "@/helpers/delayedAction"; + +/** + * Deep-link adapter for the Stellar CLI sign flow. + * + * The CLI opens `/transaction/cli-sign?xdr=...&networkPassphrase=...`. We + * select the matching network, hand the XDR off to the import flow store, and + * redirect to the import page — which parses the envelope and renders the + * transaction overview. + */ export default function CliSignTransaction() { - const { network, updateIsDynamicNetworkSelect, transaction, selectNetwork } = - useStore(); + const { updateIsDynamicNetworkSelect, selectNetwork } = useStore(); + const { setImportXdr } = useImportFlowStore(); const searchParams = useSearchParams(); - - const getNetworkByPassphrase = (passphrase: string) => { - return NetworkOptions.find((network) => network.passphrase === passphrase); - }; const router = useRouter(); + const getNetworkByPassphrase = (passphrase: string) => + NetworkOptions.find((network) => network.passphrase === passphrase); + useEffect(() => { const networkPassphrase = searchParams?.get("networkPassphrase"); const xdr = searchParams?.get("xdr"); @@ -32,16 +41,21 @@ export default function CliSignTransaction() { } if (xdr) { - transaction.updateSignActiveView("overview"); - transaction.updateSignImportXdr(xdr); + setImportXdr(xdr); } trackEvent(TrackingEvent.TRANSACTION_CLI_SIGN); - router.push(Routes.IMPORT_TRANSACTION); - // Not including other deps + delayedAction({ + action() { + router.push(Routes.IMPORT_TRANSACTION); + }, + delay: 0, + }); + + // Run once for the deep link's search params; the import page owns parsing. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [transaction.sign.importXdr, network.id]); + }, [searchParams]); return ; } diff --git a/src/app/(sidebar)/transaction/import/components/ImportStepContent.tsx b/src/app/(sidebar)/transaction/import/components/ImportStepContent.tsx index 1a9a16249..ed904b71a 100644 --- a/src/app/(sidebar)/transaction/import/components/ImportStepContent.tsx +++ b/src/app/(sidebar)/transaction/import/components/ImportStepContent.tsx @@ -1,5 +1,6 @@ "use client"; +import { useEffect } from "react"; import { FeeBumpTransaction, Transaction, @@ -12,9 +13,8 @@ import { useStore } from "@/store/useStore"; import { useImportSignatureCompleteness } from "@/hooks/useImportSignatureCompleteness"; -import { hasSorobanData, isSorobanOperationType } from "@/helpers/sorobanUtils"; +import { parseImportXdr, ParsedImportXdr } from "@/helpers/parseImportXdr"; -import { validate } from "@/validate"; import { trackEvent, TrackingEvent } from "@/metrics/tracking"; import { FEE_BUMP_TX_FIELDS, TX_FIELDS } from "@/constants/signTransactionPage"; @@ -80,63 +80,46 @@ export const ImportStepContent = ({ } })(); + // Push parse-derived fields into the import flow store as a unit so every + // entry point (paste here, or the cli-sign deep link) leaves identical state. + const applyParseResult = (result: ParsedImportXdr) => { + setImportParsedType(result.parsedTxType); + setImportHasSignatures(result.hasSignatures); + setImportIsSimulated(result.isSimulated); + setImportIsFeeBump(result.isFeeBump); + setImportParseError(result.parseError); + }; + const onChange = (value: string) => { setImportXdr(value); - if (!value) { - setImportParseError(null); - setImportParsedType(null); - setImportHasSignatures(false); - setImportIsSimulated(false); - setImportIsFeeBump(false); - return; + if (value) { + trackEvent(TrackingEvent.TRANSACTION_IMPORT_XDR_PASTE); } - trackEvent(TrackingEvent.TRANSACTION_IMPORT_XDR_PASTE); + const result = parseImportXdr(value, network.passphrase); + applyParseResult(result); - const xdrValidation = validate.getXdrError(value); - - if (xdrValidation?.result === "error") { - setImportParseError(xdrValidation.message ?? "Invalid XDR"); - setImportParsedType(null); - setImportHasSignatures(false); - setImportIsSimulated(false); - setImportIsFeeBump(false); - trackEvent(TrackingEvent.TRANSACTION_IMPORT_XDR_INVALID); - return; - } - - try { - const tx = TransactionBuilder.fromXDR(value, network.passphrase) as - | Transaction - | FeeBumpTransaction; - - const operations = isFeeBumpTransaction(tx) - ? tx.innerTransaction.operations - : tx.operations; - - const isSoroban = isSorobanOperationType(operations?.[0]?.type ?? ""); - - setImportParsedType(isSoroban ? "soroban" : "classic"); - setImportHasSignatures(tx.signatures.length > 0); - setImportIsSimulated(isSoroban && hasSorobanData(tx)); - setImportIsFeeBump(isFeeBumpTransaction(tx)); - setImportParseError(null); - trackEvent(TrackingEvent.TRANSACTION_IMPORT_XDR_VALID); - } catch (e) { - setImportParseError( - e instanceof Error - ? e.message - : "Unable to parse transaction envelope XDR", + if (value) { + trackEvent( + result.parseError + ? TrackingEvent.TRANSACTION_IMPORT_XDR_INVALID + : TrackingEvent.TRANSACTION_IMPORT_XDR_VALID, ); - setImportParsedType(null); - setImportHasSignatures(false); - setImportIsSimulated(false); - setImportIsFeeBump(false); - trackEvent(TrackingEvent.TRANSACTION_IMPORT_XDR_INVALID); } }; + // Parse XDR that arrived from an external entry point — e.g. the CLI deep + // link at `/transaction/cli-sign`, which sets only `importXdr` before + // redirecting here. Without this, the derived fields stay empty and the + // overview never renders. Runs once on mount; pasting goes through onChange. + useEffect(() => { + if (importXdr && !parsedTxType && !parseError) { + applyParseResult(parseImportXdr(importXdr, network.passphrase)); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const renderSuccessImportAlert = () => { if (isMultisigDeferred) { return ( diff --git a/src/helpers/parseImportXdr.ts b/src/helpers/parseImportXdr.ts new file mode 100644 index 000000000..80138a868 --- /dev/null +++ b/src/helpers/parseImportXdr.ts @@ -0,0 +1,88 @@ +import { + FeeBumpTransaction, + Transaction, + TransactionBuilder, +} from "@stellar/stellar-sdk"; + +import { hasSorobanData, isSorobanOperationType } from "@/helpers/sorobanUtils"; +import { validate } from "@/validate"; + +/** Parse-derived state for an imported transaction envelope. */ +export type ParsedImportXdr = { + parsedTxType: "classic" | "soroban" | null; + hasSignatures: boolean; + isSimulated: boolean; + isFeeBump: boolean; + parseError: string | null; +}; + +const EMPTY_RESULT: ParsedImportXdr = { + parsedTxType: null, + hasSignatures: false, + isSimulated: false, + isFeeBump: false, + parseError: null, +}; + +const isFeeBumpTransaction = ( + tx: Transaction | FeeBumpTransaction, +): tx is FeeBumpTransaction => "innerTransaction" in tx; + +/** + * Parse a transaction envelope XDR into the fields the import flow stores. + * + * For the import page's `onChange` andexternal entry points + * (e.g. the CLI deep link at `/transaction/cli-sign`) + * without duplicating the parse logic — keeping the derived state identical + * regardless of how the XDR arrived. + * + * @param value - The transaction envelope XDR (base64). + * @param networkPassphrase - Network passphrase used to construct the tx. + * @returns Parsed type, signature presence, simulation/fee-bump flags, and a + * parse error message when the XDR can't be decoded. + */ +export const parseImportXdr = ( + value: string, + networkPassphrase: string, +): ParsedImportXdr => { + if (!value) { + return { ...EMPTY_RESULT }; + } + + const xdrValidation = validate.getXdrError(value); + + if (xdrValidation?.result === "error") { + return { + ...EMPTY_RESULT, + parseError: xdrValidation.message ?? "Invalid XDR", + }; + } + + try { + const tx = TransactionBuilder.fromXDR(value, networkPassphrase) as + | Transaction + | FeeBumpTransaction; + + const operations = isFeeBumpTransaction(tx) + ? tx.innerTransaction.operations + : tx.operations; + + const isSoroban = isSorobanOperationType(operations?.[0]?.type ?? ""); + + return { + parsedTxType: isSoroban ? "soroban" : "classic", + hasSignatures: tx.signatures.length > 0, + isSimulated: isSoroban && hasSorobanData(tx), + isFeeBump: isFeeBumpTransaction(tx), + parseError: null, + }; + } catch (e) { + return { + ...EMPTY_RESULT, + parseError: + e instanceof Error + ? e.message + : "Unable to parse transaction envelope XDR", + }; + } +}; From a9ccdeeff6c2552c374c86a14794df76287768de Mon Sep 17 00:00:00 2001 From: Jeesun Kim Date: Thu, 11 Jun 2026 16:20:58 -0700 Subject: [PATCH 4/6] fix tests --- tests/e2e/feeBumpPage.test.ts | 2 +- tests/e2e/signStepContent.test.ts | 4 ++-- tests/e2e/signerSelector.test.ts | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/e2e/feeBumpPage.test.ts b/tests/e2e/feeBumpPage.test.ts index 178fced19..07a0f6834 100644 --- a/tests/e2e/feeBumpPage.test.ts +++ b/tests/e2e/feeBumpPage.test.ts @@ -60,7 +60,7 @@ test.describe("Fee Bump Page", () => { await expect(nextButton).toBeVisible(); await nextButton.click(); - await expect(page.locator("h1")).toHaveText("Import transaction"); + await expect(page.locator("h1")).toHaveText("Sign transaction"); }); test("View in XDR viewer", async ({ page }) => { diff --git a/tests/e2e/signStepContent.test.ts b/tests/e2e/signStepContent.test.ts index 443b18bb6..21f874436 100644 --- a/tests/e2e/signStepContent.test.ts +++ b/tests/e2e/signStepContent.test.ts @@ -74,7 +74,7 @@ test.describe("Sign Step in Build Flow", () => { }) => { await seedSessionStorageAndNavigate(page); - await expect(page.locator("h1")).toHaveText("Import transaction"); + await expect(page.locator("h1")).toHaveText("Sign transaction"); await expect( page.getByText( "To be included in the ledger, the transaction must be signed and submitted to the network.", @@ -100,7 +100,7 @@ test.describe("Sign Step in Build Flow", () => { await seedSessionStorageAndNavigate(page); // Wait for the sign step to hydrate from sessionStorage - await expect(page.locator("h1")).toHaveText("Import transaction"); + await expect(page.locator("h1")).toHaveText("Sign transaction"); const nextButton = page.locator('[data-position="right"]'); await expect(nextButton).toBeDisabled(); diff --git a/tests/e2e/signerSelector.test.ts b/tests/e2e/signerSelector.test.ts index 4373797d1..a1cba82ea 100644 --- a/tests/e2e/signerSelector.test.ts +++ b/tests/e2e/signerSelector.test.ts @@ -93,25 +93,26 @@ test.describe("Signer Selector", () => { test("'Use secret key' dropdown works for source account", async () => { // Import transaction await pageContext - .getByLabel("Import a transaction envelope in XDR format") + .getByLabel("Transaction envelope in XDR") .fill(MOCK_TX_XDR); await expect( - pageContext.getByText("Valid Transaction Envelope XDR"), + pageContext.getByText("Transaction imported successfully"), ).toBeVisible(); + + // Advance to the Sign step await pageContext - .getByRole("button", { name: "Import transaction" }) + .getByRole("button", { name: "Sign transaction", exact: true }) .click(); - // Verify overview is visible - await expect(pageContext.getByTestId("sign-tx-overview")).toBeVisible(); - // First signer - await pageContext.getByText("Use secret key").click(); + await pageContext.getByText("Use secret key").first().click(); const { values } = await validateSignerSelectorOptions(pageContext); await values.nth(0).click(); - const multipickers = pageContext.getByTestId("multipicker-signer"); + const multipickers = pageContext.getByTestId( + "multipicker-sign-step-signer", + ); const multiPickerInputs = multipickers.locator(".Input"); await expect(multiPickerInputs.nth(0).locator("input")).toHaveValue( SAVED_ACCOUNT_1_SECRET, @@ -138,8 +139,7 @@ test.describe("Signer Selector", () => { // Sign transaction await pageContext - .getByTestId("sign-tx-secretkeys") - .getByText("Sign transaction") + .getByRole("button", { name: "Sign", exact: true }) .click(); await expect( From e08f9e6f67c7fce0cd0c1f09bc5e2a8a9f57665f Mon Sep 17 00:00:00 2001 From: Jeesun Kim Date: Thu, 11 Jun 2026 16:53:00 -0700 Subject: [PATCH 5/6] remove '/transaction/sign' from urlParams.test --- tests/e2e/urlParams.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/e2e/urlParams.test.ts b/tests/e2e/urlParams.test.ts index 747190179..0271c2543 100644 --- a/tests/e2e/urlParams.test.ts +++ b/tests/e2e/urlParams.test.ts @@ -112,24 +112,6 @@ test.describe("URL Params", () => { ); }); - test("[Classic] Sign Transaction", async ({ page }) => { - await page.goto( - `${baseURL}/transaction/sign?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;&transaction$sign$activeView=overview&importXdr=AAAAAgAAAAA55ZjOXdOOulfzeLPXjLDLdplq//5HGjapWAXjGSkdAkwAAD6AADQioAAAAAQAAAAEAAAAAAAAAAAAAAABnUbvoAAAAAQAAAAMxMjMAAAAAAgAAAAAAAAAAAAAAALs4fndRzE6mDMvxXqgyh79//PxCtOwb9MTWEeINa//Qr8AAAABvwjrAAAAAABAAAAADnlmM5d0466V//N4s9eMsMt2mWr//kcaNqlYBeMZKR0CTAAAAAQAAAAASBB3qbNO//amClp01Lvg//fRcZsxzvl0ItXd0lfm+7+ggAAAAFVU0RDAAAAAEI+fQXy7K+//7BkrIVo//G+lq7bjY5wJUq+NBPgIH3layAAAACVAvkAAAAAAAAAAAAA==;;`, - ); - - await expect(page.locator("h1")).toHaveText("Transaction overview"); - - await expect(page.getByLabel("Signing for")).toHaveValue( - "Test SDF Network ; September 2015", - ); - await expect(page.getByLabel("Transaction Envelope XDR")).toHaveValue( - "AAAAAgAAAAA55ZjOXdOOulfzeLPXjLDLdplq/5HGjapWAXjGSkdAkwAAD6AADQioAAAAAQAAAAEAAAAAAAAAAAAAAABnUbvoAAAAAQAAAAMxMjMAAAAAAgAAAAAAAAAAAAAAALs4fndRzE6mDMvxXqgyh79/PxCtOwb9MTWEeINa/Qr8AAAABvwjrAAAAAABAAAAADnlmM5d0466V/N4s9eMsMt2mWr/kcaNqlYBeMZKR0CTAAAAAQAAAAASBB3qbNO/amClp01Lvg/fRcZsxzvl0ItXd0lfm+7+ggAAAAFVU0RDAAAAAEI+fQXy7K+/7BkrIVo/G+lq7bjY5wJUq+NBPgIH3layAAAACVAvkAAAAAAAAAAAAA==", - ); - await expect(page.getByLabel("Transaction hash")).toHaveValue( - "44abaabac11c318d595d392c24166965301b48109899bc8e819723afb89d5e37", - ); - }); - test("Simulate Transaction redirects to build", async ({ page }) => { await page.goto( `${baseURL}/transaction/simulate?$=network$id=testnet&label=Testnet&horizonUrl=https:////horizon-testnet.stellar.org&rpcUrl=https:////soroban-testnet.stellar.org&passphrase=Test%20SDF%20Network%20/;%20September%202015;`, From 818e4b44696e8a50e19b97a182b91fa16865cade Mon Sep 17 00:00:00 2001 From: Jeesun Kim Date: Fri, 26 Jun 2026 16:30:49 -0700 Subject: [PATCH 6/6] add reset for cli redirect --- src/app/(sidebar)/transaction/cli-sign/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(sidebar)/transaction/cli-sign/page.tsx b/src/app/(sidebar)/transaction/cli-sign/page.tsx index 704a68d25..48e59a790 100644 --- a/src/app/(sidebar)/transaction/cli-sign/page.tsx +++ b/src/app/(sidebar)/transaction/cli-sign/page.tsx @@ -21,7 +21,7 @@ import { delayedAction } from "@/helpers/delayedAction"; */ export default function CliSignTransaction() { const { updateIsDynamicNetworkSelect, selectNetwork } = useStore(); - const { setImportXdr } = useImportFlowStore(); + const { resetAll, setImportXdr } = useImportFlowStore(); const searchParams = useSearchParams(); const router = useRouter(); @@ -41,6 +41,7 @@ export default function CliSignTransaction() { } if (xdr) { + resetAll(); setImportXdr(xdr); }