Skip to content

Commit f4873d3

Browse files
authored
[New Tx Flow] Add Validate Step (#1984)
1 parent 843a1d0 commit f4873d3

File tree

9 files changed

+639
-34
lines changed

9 files changed

+639
-34
lines changed

src/app/(sidebar)/transaction/build/components/SimulateStepContent.tsx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import {
88
Icon,
99
Link,
1010
Input,
11-
Select,
1211
} from "@stellar/design-system";
1312

1413
import { useBuildFlowStore } from "@/store/createTransactionFlowStore";
1514
import { useStore } from "@/store/useStore";
1615

1716
import { useSimulateTx } from "@/query/useSimulateTx";
1817

18+
import { AuthModePicker } from "@/components/FormElements/AuthModePicker";
1919
import { XdrPicker } from "@/components/FormElements/XdrPicker";
2020
import { Box } from "@/components/layout/Box";
2121
import { PageCard } from "@/components/layout/PageCard";
@@ -230,42 +230,24 @@ export const SimulateStepContent = () => {
230230
/>
231231

232232
{/* Auth mode selector */}
233-
<Select
234-
id="ledger-key-type"
235-
fieldSize="md"
236-
label="Auth mode"
233+
<AuthModePicker
234+
id="simulate-auth-mode"
237235
value={authMode}
238-
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
236+
onChange={(mode) => {
239237
resetSimulateTx();
240-
241-
const selectedVal = e.target.value;
242-
selectAuthMode(selectedVal);
238+
selectAuthMode(mode);
243239
}}
244240
note={
245241
<>
246242
This simulation shows which signatures are required. It doesn’t
247243
validate signatures or calculate final fees.{" "}
248-
{
249-
<SdsLink href="https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#authorization">
250-
Learn more
251-
</SdsLink>
252-
}
244+
<SdsLink href="https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#authorization">
245+
Learn more
246+
</SdsLink>
253247
.
254248
</>
255249
}
256-
>
257-
<option value="">Select a key</option>
258-
259-
{[
260-
{ id: "record", label: "Record" },
261-
{ id: "enforce", label: "Enforce" },
262-
{ id: "record-allow-nonroot", label: "Record (allow non-root)" },
263-
].map((f) => (
264-
<option key={f.id} value={f.id}>
265-
{f.label}
266-
</option>
267-
))}
268-
</Select>
250+
/>
269251

270252
{/* Simulate button */}
271253
<Box gap="md" direction="row">
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import {
5+
Alert,
6+
Button,
7+
Card,
8+
Icon,
9+
Link,
10+
} from "@stellar/design-system";
11+
12+
import { useBuildFlowStore } from "@/store/createTransactionFlowStore";
13+
import { useStore } from "@/store/useStore";
14+
15+
import { useSimulateTx } from "@/query/useSimulateTx";
16+
17+
import { AuthModePicker } from "@/components/FormElements/AuthModePicker";
18+
import { XdrPicker } from "@/components/FormElements/XdrPicker";
19+
import { Box } from "@/components/layout/Box";
20+
import { PageCard } from "@/components/layout/PageCard";
21+
import { PageHeader } from "@/components/layout/PageHeader";
22+
import { CodeEditor } from "@/components/CodeEditor";
23+
import { ExpandBox } from "@/components/ExpandBox";
24+
import { SdsLink } from "@/components/SdsLink";
25+
import { SorobanAuthSigningCard } from "@/components/SorobanAuthSigning";
26+
27+
import { getNetworkHeaders } from "@/helpers/getNetworkHeaders";
28+
import { extractAuthEntries } from "@/helpers/sorobanAuthUtils";
29+
30+
import { trackEvent, TrackingEvent } from "@/metrics/tracking";
31+
32+
import {
33+
SimulationResourceTable,
34+
getSimulationResourceInfo,
35+
} from "./SimulationResourceTable";
36+
37+
/**
38+
* Validate step content for the single-page transaction flow (Soroban only).
39+
*
40+
* Runs an enforce-mode re-simulation of the signed transaction to verify that
41+
* auth entry signatures are valid before submitting. On success, stores the
42+
* validated result so the user can proceed to submit.
43+
*
44+
* @see https://developers.stellar.org/docs/build/guides/transactions/signing-soroban-invocations#how-it-works-1
45+
* @see https://github.com/stellar/stellar-protocol/blob/master/core/cap-0071.md
46+
*
47+
* @example
48+
* {activeStep === "validate" && <ValidateStepContent />}
49+
*/
50+
export const ValidateStepContent = () => {
51+
const { network } = useStore();
52+
const {
53+
build,
54+
simulate,
55+
sign,
56+
validate,
57+
setValidateResult,
58+
setValidateAuthMode,
59+
setValidatedXdr,
60+
setSignedAuthEntriesXdr,
61+
} = useBuildFlowStore();
62+
63+
const [isResourcesExpanded, setIsResourcesExpanded] = useState(false);
64+
const [validationDisplay, setValidationDisplay] = useState<string>("");
65+
66+
// The signed transaction envelope XDR from the Sign step
67+
const signedXdr = sign.signedXdr;
68+
69+
const {
70+
mutateAsync: simulateTx,
71+
data: simulateTxData,
72+
error: simulateTxError,
73+
isPending: isSimulateTxPending,
74+
reset: resetSimulateTx,
75+
} = useSimulateTx();
76+
77+
const authMode = validate?.authMode || "enforce";
78+
79+
const isActionDisabled = !network.rpcUrl || !signedXdr;
80+
81+
/**
82+
* Run enforce-mode re-simulation to validate auth entry signatures.
83+
*/
84+
const onValidate = async () => {
85+
if (!network.rpcUrl || !signedXdr) return;
86+
87+
try {
88+
const response = await simulateTx({
89+
rpcUrl: network.rpcUrl,
90+
transactionXdr: signedXdr,
91+
headers: getNetworkHeaders(network, "rpc"),
92+
xdrFormat: "base64",
93+
authMode,
94+
});
95+
96+
if (response) {
97+
const resultJson = JSON.stringify(response, null, 2);
98+
setValidationDisplay(resultJson);
99+
setValidateResult(resultJson);
100+
101+
const hasError = Boolean(response?.error || response?.result?.error);
102+
103+
if (!hasError) {
104+
// Store the signed XDR as the validated XDR — enforce sim confirms
105+
// the auth signatures are valid and the transaction is ready to submit
106+
setValidatedXdr(signedXdr);
107+
trackEvent(TrackingEvent.TRANSACTION_VALIDATE_AUTH_ENFORCE_SUCCESS);
108+
} else {
109+
trackEvent(TrackingEvent.TRANSACTION_VALIDATE_AUTH_ENFORCE_FAILURE);
110+
}
111+
}
112+
} catch {
113+
trackEvent(TrackingEvent.TRANSACTION_VALIDATE_AUTH_ENFORCE_FAILURE);
114+
}
115+
};
116+
117+
const hasValidationResult = Boolean(simulateTxData);
118+
const hasError = Boolean(
119+
simulateTxError || simulateTxData?.error || simulateTxData?.result?.error,
120+
);
121+
const isValidationSuccess = hasValidationResult && !hasError;
122+
const resourceInfo = getSimulationResourceInfo(simulateTxData);
123+
const authEntries = simulateTxData ? extractAuthEntries(simulateTxData) : [];
124+
const hasAuthEntries = authEntries.length > 0;
125+
const builtXdr = build.soroban.xdr || build.classic.xdr;
126+
127+
return (
128+
<Box gap="md">
129+
<PageHeader heading="Validate auth entries" as="h1" />
130+
131+
<PageCard>
132+
{!network.rpcUrl ? (
133+
<Alert variant="warning" placement="inline" title="Attention">
134+
RPC URL is required to validate auth entries. You can add it in the
135+
network settings in the upper right corner.
136+
</Alert>
137+
) : null}
138+
139+
<Box gap="lg">
140+
<XdrPicker
141+
id="validate-xdr-blob"
142+
label="Signed transaction XDR"
143+
value={signedXdr}
144+
disabled
145+
hasCopyButton
146+
/>
147+
148+
{/* Auth mode selector */}
149+
<AuthModePicker
150+
id="validate-auth-mode"
151+
value={authMode}
152+
onChange={(mode) => {
153+
resetSimulateTx();
154+
setValidateAuthMode(mode);
155+
}}
156+
note={
157+
<>
158+
Re-simulates the signed transaction to validate authorization
159+
entry signatures.{" "}
160+
<SdsLink href="https://developers.stellar.org/docs/build/guides/transactions/signing-soroban-invocations#how-it-works-1">
161+
Learn more
162+
</SdsLink>
163+
.
164+
</>
165+
}
166+
/>
167+
168+
{/* Validate button */}
169+
<Box gap="md" direction="row">
170+
<Button
171+
disabled={Boolean(isActionDisabled)}
172+
isLoading={isSimulateTxPending}
173+
size="md"
174+
variant="secondary"
175+
onClick={() => {
176+
resetSimulateTx();
177+
onValidate();
178+
}}
179+
>
180+
Validate
181+
</Button>
182+
</Box>
183+
</Box>
184+
</PageCard>
185+
186+
{/* Validation result */}
187+
{hasValidationResult && (
188+
<Card>
189+
<Box gap="md">
190+
{/* Success alert */}
191+
{isValidationSuccess && (
192+
<Alert
193+
variant="success"
194+
placement="inline"
195+
title="Auth entries validated"
196+
icon={<Icon.CheckCircle />}
197+
>
198+
All authorization entry signatures are valid. The transaction is
199+
ready to submit.
200+
</Alert>
201+
)}
202+
{/* Error alert */}
203+
{hasError && (
204+
<Alert
205+
variant="error"
206+
placement="inline"
207+
title="Validation failed"
208+
>
209+
{""}
210+
</Alert>
211+
)}
212+
<div data-testid="validate-step-response">
213+
<CodeEditor
214+
title="Validation Result"
215+
value={validationDisplay}
216+
selectedLanguage="json"
217+
maxHeightInRem="20"
218+
/>
219+
</div>
220+
221+
{/* Resource usage (collapsible) */}
222+
{resourceInfo && (
223+
<Box gap="sm">
224+
<div
225+
className="SimulateStepContent__expand-toggle"
226+
onClick={() => setIsResourcesExpanded(!isResourcesExpanded)}
227+
data-is-expanded={isResourcesExpanded}
228+
>
229+
<Link size="sm" icon={<Icon.ChevronDown />}>
230+
View resources and fees from validation
231+
</Link>
232+
</div>
233+
234+
<ExpandBox isExpanded={isResourcesExpanded} offsetTop="sm">
235+
<SimulationResourceTable resourceInfo={resourceInfo} />
236+
</ExpandBox>
237+
</Box>
238+
)}
239+
240+
{/* Auth entry signing card */}
241+
{hasAuthEntries && (
242+
<SorobanAuthSigningCard
243+
authEntriesXdr={authEntries}
244+
signedAuthEntriesXdr={simulate.signedAuthEntriesXdr || []}
245+
builtXdr={builtXdr}
246+
onAuthSigned={({ signedXdr: signed }) => {
247+
if (signed) {
248+
setSignedAuthEntriesXdr(authEntries);
249+
}
250+
}}
251+
/>
252+
)}
253+
</Box>
254+
</Card>
255+
)}
256+
</Box>
257+
);
258+
};

src/app/(sidebar)/transaction/build/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ClassicTransactionXdr } from "./components/ClassicTransactionXdr";
2121
import { SorobanTransactionXdr } from "./components/SorobanTransactionXdr";
2222
import { SimulateStepContent } from "./components/SimulateStepContent";
2323
import { SignStepContent } from "./components/SignStepContent";
24+
import { ValidateStepContent } from "./components/ValidateStepContent";
2425

2526
import "./styles.scss";
2627

@@ -29,6 +30,7 @@ export default function BuildTransaction() {
2930
build,
3031
simulate,
3132
sign,
33+
validate,
3234
activeStep,
3335
highestCompletedStep,
3436
setActiveStep,
@@ -44,8 +46,14 @@ export default function BuildTransaction() {
4446
const { soroban } = build;
4547
const isSoroban = Boolean(soroban.operation.operation_type);
4648

49+
const hasAuthEntries = Boolean(
50+
simulate.authEntriesXdr && simulate.authEntriesXdr.length > 0,
51+
);
52+
4753
const steps: TransactionStepName[] = isSoroban
48-
? ["build", "simulate", "sign", "submit"]
54+
? hasAuthEntries
55+
? ["build", "simulate", "sign", "validate", "submit"]
56+
: ["build", "simulate", "sign", "submit"]
4957
: ["build", "sign", "submit"];
5058

5159
const { handleNext, handleBack, handleStepClick } = useTransactionFlow({
@@ -76,6 +84,9 @@ export default function BuildTransaction() {
7684
if (activeStep === "sign") {
7785
return !sign.signedXdr;
7886
}
87+
if (activeStep === "validate") {
88+
return !validate?.validatedXdr;
89+
}
7990
return false;
8091
};
8192

@@ -185,6 +196,7 @@ export default function BuildTransaction() {
185196
{activeStep === "build" && renderBuildStep()}
186197
{activeStep === "simulate" && <SimulateStepContent />}
187198
{activeStep === "sign" && <SignStepContent />}
199+
{activeStep === "validate" && <ValidateStepContent />}
188200

189201
<TransactionFlowFooter
190202
steps={steps}

0 commit comments

Comments
 (0)