Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/app/(sidebar)/transaction/build/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { Notification, Card } from "@stellar/design-system";

import { useBuildFlowStore } from "@/store/createTransactionFlowStore";
Expand Down Expand Up @@ -39,6 +39,7 @@ export default function BuildTransaction() {
setActiveStep,
goToNextStep,
markStepCompleted,
resetDownstreamState,
resetAll,
} = useBuildFlowStore();

Expand Down Expand Up @@ -103,6 +104,39 @@ export default function BuildTransaction() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isNextDisabled, activeStep]);

// When the user edits the transaction on the build step after having already
// progressed past it, the rebuilt XDR no longer matches what was simulated,
// signed, or validated downstream — those results are now stale. Reset them
// so a signature produced against the previous transaction can't be carried
// through to submit; the user must re-run the later steps against the edited
// transaction.
const prevBuiltXdrRef = useRef<string | null>(null);
useEffect(() => {
if (!currentXdr) {
return;
}

if (prevBuiltXdrRef.current === null) {
prevBuiltXdrRef.current = currentXdr;
return;
}
if (prevBuiltXdrRef.current === currentXdr) {
return;
}
prevBuiltXdrRef.current = currentXdr;

const buildIndex = steps.indexOf("build");
const highestIndex = highestCompletedStep
? steps.indexOf(highestCompletedStep)
: -1;

// Only reset when there is downstream progress to invalidate.
if (highestIndex > buildIndex) {
resetDownstreamState(steps[buildIndex + 1], steps);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentXdr]);

const renderError = () => {
if (paramsError.length > 0 || operationsError.length > 0) {
return (
Expand Down
76 changes: 57 additions & 19 deletions src/app/(sidebar)/transaction/components/SignStepContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,31 @@
import { useState } from "react";
import { Notification, Card, Text } from "@stellar/design-system";

import { useStore } from "@/store/useStore";

import { decodeXdr } from "@/helpers/decodeXdr";

import { useIsXdrInit } from "@/hooks/useIsXdrInit";

import { SignTransactionXdr } from "@/components/SignTransactionXdr";
import { Box } from "@/components/layout/Box";
import { TransactionHashReadOnlyField } from "@/components/TransactionHashReadOnlyField";
import { prettifyJsonString } from "@/helpers/prettifyJsonString";

import { TransactionStepHeader } from "./TransactionStepHeader";
import { CodeEditor } from "@/components/CodeEditor";

type Props = {
xdrToSign: string;
signedXdr: string;
onSigned: (signedXdr: string) => void;
onClearAll: () => void;
/**
* Optional slot rendered above the signing UI — the import flow passes a
* signature-status panel here so a co-signer can review existing signatures
* before adding their own. Omitted by the build flow.
*/
signatureContext?: React.ReactNode;
};

/**
Expand All @@ -38,8 +53,25 @@ export const SignStepContent = ({
signedXdr,
onSigned,
onClearAll,
signatureContext,
}: Props) => {
const { network } = useStore();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [selectedLanguage, setSelectedLanguage] = useState<"json" | "xdr">(
"json",
);

const isXdrInit = useIsXdrInit();

const xdrJsonDecoded = decodeXdr({
xdrType: "TransactionEnvelope",
xdrBlob: xdrToSign,
isReady: isXdrInit,
});

const signedXdrJsonString = xdrJsonDecoded?.jsonString
? `${prettifyJsonString(xdrJsonDecoded.jsonString)}\n`
: "";

return (
<Box gap="md">
Expand All @@ -54,6 +86,8 @@ export const SignStepContent = ({
submitted to the network.
</Text>

{signatureContext}

<SignTransactionXdr
id="sign-step"
xdrToSign={xdrToSign || null}
Expand All @@ -77,25 +111,29 @@ export const SignStepContent = ({
</Notification>

<Box gap="xxl">
<Box gap="xs">
<Text
size="xs"
weight="medium"
as="div"
addlClassName="SignStepContent__label"
>
Signed transaction (Base64 XDR)
</Text>

<div className="SignStepContent__xdrBox">
<Text
size="sm"
as="div"
addlClassName="SignStepContent__xdrText"
>
{signedXdr}
</Text>
</div>
<Box gap="lg">
<TransactionHashReadOnlyField
xdr={signedXdr}
networkPassphrase={network.passphrase}
/>

{signedXdrJsonString ? (
<CodeEditor
title="Signed transaction"
value={
selectedLanguage === "json"
? signedXdrJsonString
: signedXdr
}
languages={["json", "xdr"]}
selectedLanguage={selectedLanguage}
onLanguageChange={(id) => {
const selectedValue = id === "xdr" ? "xdr" : "json";
setSelectedLanguage(selectedValue);
}}
maxHeightInRem="20"
/>
) : null}
</Box>
</Box>
</Box>
Expand Down
10 changes: 6 additions & 4 deletions src/app/(sidebar)/transaction/components/Signatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ const getEnvelopeSummary = (
if (hasUnrecognized) {
return {
message:
"Includes signature(s) from signers that cant be verified offline (e.g. multisig cosigners). Submit to verify.",
"Includes signature(s) that can't be verified offline (e.g. from multisig cosigners)",
};
}
return {
message: `Missing signature${missing.length > 1 ? "s" : ""} from ${missing.join(", ")}.`,
message: `Couldn’t verify a signature for ${missing.join(", ")} offline. If it’s a multisig account, an existing signature may already cover it on-chain — you can submit to let the network verify, or add a signature first.`,
};
}

Expand All @@ -106,7 +106,9 @@ const getEnvelopeSummary = (
);
}
if (hasUnrecognized) {
notes.push("Signature(s) from unrecognized signers were also found.");
notes.push(
"Signature(s) from unverified existing signers were also found.",
);
}

return {
Expand Down Expand Up @@ -339,7 +341,7 @@ const renderSigner = (matchStatus: MatchStatus, signer?: string) => {
return (
<Box gap="xs" direction="row" align="center" addlClassName="info-message">
<Icon.InfoCircle />
<span>Unrecognized signer</span>
<span>Existing signer (unverified)</span>
</Box>
);
};
27 changes: 18 additions & 9 deletions src/app/(sidebar)/transaction/components/SubmitStepContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import { useSubmitHorizonTx } from "@/query/useSubmitHorizonTx";
import { Box } from "@/components/layout/Box";
import { XdrPicker } from "@/components/FormElements/XdrPicker";
import { TransactionHashReadOnlyField } from "@/components/TransactionHashReadOnlyField";
import { CodeEditor } from "@/components/CodeEditor";
import { ValidationResponseCard } from "@/components/ValidationResponseCard";
import { TxResponse } from "@/components/TxResponse";
import {
Expand All @@ -32,6 +31,7 @@ import {
} from "@/components/TxErrorResponse";
import { XdrLink } from "@/components/XdrLink";
import { TxHashLink } from "@/components/TxHashLink";
import { PrettyJsonTransaction } from "@/components/PrettyJsonTransaction";

import { getNetworkHeaders } from "@/helpers/getNetworkHeaders";
import { getBlockExplorerLink } from "@/helpers/getBlockExplorerLink";
Expand All @@ -40,6 +40,7 @@ import { delayedAction } from "@/helpers/delayedAction";
import { localStorageSettings } from "@/helpers/localStorageSettings";
import * as StellarXdr from "@/helpers/StellarXdr";
import { buildEndpointHref } from "@/helpers/buildEndpointHref";
import { parseToLosslessJson } from "@/helpers/parseToLosslessJson";

import { useScrollIntoView } from "@/hooks/useScrollIntoView";

Expand Down Expand Up @@ -145,16 +146,22 @@ export const SubmitStepContent = ({
);
return {
jsonString: JSON.stringify(JSON.parse(jsonString), null, 2),
jsonObject: parseToLosslessJson(jsonString),
error: "",
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return { jsonString: "", error: "Unable to decode XDR" };
return {
jsonString: "",
jsonObject: null,
error: "Unable to decode XDR",
};
}
}, [xdrBlob]);

const [xdrJson, setXdrJson] = useState<{
jsonString: string;
jsonObject: Record<string, unknown> | null;
error: string;
} | null>(null);

Expand Down Expand Up @@ -580,13 +587,15 @@ export const SubmitStepContent = ({
networkPassphrase={network.passphrase}
/>

{xdrJson?.jsonString ? (
<CodeEditor
title="Transaction envelope"
value={xdrJson.jsonString}
selectedLanguage="json"
/>
) : null}
<div className="PageBody__content">
{xdrJson?.jsonObject ? (
<PrettyJsonTransaction
json={xdrJson.jsonObject}
xdr={xdrBlob}
isCodeWrapped={true}
/>
Comment on lines +592 to +596
) : null}
</div>
</Box>
<Box gap="sm" direction="row" align="center" justify="left">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import { Notification } from "@stellar/design-system";
import { useImportFlowStore } from "@/store/createTransactionFlowStore";
import { useStore } from "@/store/useStore";

import { useImportSignatureCompleteness } from "@/hooks/useImportSignatureCompleteness";

import { parseImportXdr, ParsedImportXdr } from "@/helpers/parseImportXdr";

import { trackEvent, TrackingEvent } from "@/metrics/tracking";
Expand Down Expand Up @@ -64,11 +62,6 @@ export const ImportStepContent = ({
const parseError = importState?.parseError ?? null;
const parsedTxType = importState?.parsedTxType ?? null;

const signatureCompleteness = useImportSignatureCompleteness();
const isMultisigDeferred =
Boolean(isReadyToSubmit) &&
(signatureCompleteness?.missingSigners.length ?? 0) > 0;

const parsedTx: Transaction | FeeBumpTransaction | null = (() => {
if (!importXdr || parseError || !parsedTxType) return null;
try {
Expand Down Expand Up @@ -121,13 +114,6 @@ export const ImportStepContent = ({
}, []);

const renderSuccessImportAlert = () => {
if (isMultisigDeferred) {
return (
<Notification variant="primary" title="Transaction imported.">
Signatures from unrecognized signers detected. Submit to verify.
</Notification>
);
}
if (isReadyToSubmit) {
return (
<Notification
Expand Down
Loading
Loading