Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const SimulateStepContent = ({
build,
simulate,
setSimulateInstructionLeeway,
setSimulateAuthMode,
setSimulationResult,
setSimulationReadOnly,
setAuthEntriesXdr,
Expand All @@ -80,7 +81,6 @@ export const SimulateStepContent = ({
const [assemblyWarning, setAssemblyWarning] = useState("");
const [isResourcesExpanded, setIsResourcesExpanded] = useState(false);
const [xdrFormat, setXdrFormat] = useState<XdrFormatType | string>("json");
const [authMode, selectAuthMode] = useState<AuthModeType | string>("");
const [simulationDisplay, setSimulationDisplayResult] = useState<string>("");
const [validUntilLedgerSeq, setValidUntilLedgerSeq] = useState(0);

Expand Down Expand Up @@ -339,15 +339,16 @@ export const SimulateStepContent = ({
{isInvokeContract ? (
<AuthModePicker
id="simulate-auth-mode"
value={authMode}
value={simulate.authMode}
onChange={(mode) => {
resetSimulateTx();
selectAuthMode(mode);
setSimulateAuthMode(mode as AuthModeType);
}}
note={
<>
This simulation shows which signatures are required. It
doesn’t validate signatures or calculate final fees.{" "}
&quot;Record&quot; is recommended for simulation.
&quot;Record&quot; discovers which authorization entries are
required.
<SdsLink href="https://developers.stellar.org/docs/learn/fundamentals/contract-development/contract-interactions/transaction-simulation#authorization">
Learn more
</SdsLink>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const ValidateStepContent = () => {
const onValidate = async () => {
if (!network.rpcUrl || !signedXdr) return;

// Clear previous validated result so a failed re-validation can't leave
// stale validatedXdr enabling the next step.
setValidatedXdr(undefined);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


try {
const response = await simulateTx({
rpcUrl: network.rpcUrl,
Expand Down
9 changes: 5 additions & 4 deletions src/app/(sidebar)/transaction/build/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ export default function BuildTransaction() {
return !currentXdr || !(build.isValid.params && build.isValid.operations);
}
if (activeStep === "simulate") {
// Simulation must be complete. If auth entries exist, they must be signed
// (assembledXdr is set after auth signing + assembly, or after auto-assembly
// when no auth entries are present).
return !simulate.simulationResultJson;
// Simulation must be complete and assembledXdr must exist (set after auth
// signing + assembly, or after auto-assembly when no auth entries are
// present) so the sign step receives a transaction with
// simulation-derived resources/fees.
return !simulate.simulationResultJson || !simulate.assembledXdr;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
if (activeStep === "sign") {
return !sign.signedXdr;
Expand Down
2 changes: 1 addition & 1 deletion src/components/FormElements/AuthModePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AuthModeType } from "@/types/types";
const AUTH_MODE_OPTIONS: { id: string; label: string }[] = [
{ id: "record", label: "Record" },
{ id: "enforce", label: "Enforce" },
{ id: "record-allow-nonroot", label: "Record (allow non-root)" },
{ id: "record_allow_nonroot", label: "Record (allow non-root)" },
];

/**
Expand Down
10 changes: 8 additions & 2 deletions src/components/NewValidationResponseCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const NewValidationResponseCard = ({
{/* @TODO to update the SDS design system */}
<Alert
placement="inline"
variant="success"
variant={variant}
title="Your transaction envelope XDR is ready."
icon={<Icon.CheckCircle />}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the variant is dynamic, should the icon be dynamic as well?

>
Expand Down Expand Up @@ -83,7 +83,13 @@ export const NewValidationResponseCard = ({
gap="sm"
direction="row"
align="center"
justify={footerLeftEl && footerRightEl ? "space-between" : footerRightEl ? "end" : "left"}
justify={
footerLeftEl && footerRightEl
? "space-between"
: footerRightEl
? "end"
: "left"
}
addlClassName="ValidationResponseCard__footer"
wrap="wrap"
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/SorobanAuthSigning/AuthEntryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const AuthEntryItem = ({
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
trackEvent(TrackingEvent.SOROBAN_AUTH_SIGN_ENTRY_ERROR, {
entryIndex: index,
entryIndex: index + 1,
});
return { successMessage: "", errorMessage: msg };
}
Expand Down
6 changes: 4 additions & 2 deletions src/components/SorobanAuthSigning/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,11 @@ export const SorobanAuthSigningCard = ({
const signedCount = signedAuthEntriesXdr.filter(Boolean).length;
const allSigned = signedCount === entryCount && entryCount > 0;

// Trigger onAllEntriesSigned when individual signing completes all entries
// Trigger onAllEntriesSigned when individual signing completes all entries.
// Only needed in individual mode — "Sign all" calls onAllEntriesSigned
// directly in handleCustomSignAll.
useEffect(() => {
if (allSigned) {
if (allSigned && signMode === "individual") {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onAllEntriesSigned(signedAuthEntriesXdr);
}
// Only trigger when allSigned changes to true
Expand Down
3 changes: 1 addition & 2 deletions src/components/Tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,14 @@ export const Tabs = ({
const isActive = t.href ? pathname === t.href : t.id === activeTabId;
const className = `Tab ${addlClassName ?? ""}`;

if (t.href) {
if (t.href && !t.isDisabled) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return (
<Link
key={t.id}
href={t.href}
className={`external-link ${className}`}
data-testid={t.id}
data-is-active={isActive}
data-is-disabled={t.isDisabled ?? undefined}
>
{t.label}
</Link>
Expand Down
1 change: 1 addition & 0 deletions src/components/TransactionStepper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const TransactionStepper = ({

return (
<div
role="button"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

key={step}
className="TransactionStepper__step"
data-is-active={isActive || undefined}
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/sorobanUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ export const hasTypeAndValue = (v: any) => v?.type && v.value !== undefined;

/**
* Determines if the simulation result indicates a read-only transaction
* (no auth entries and no write footprint).
* (no write footprint).
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
export const checkIsReadOnly = (responseData: Record<string, any>): boolean => {
try {
Expand Down
4 changes: 2 additions & 2 deletions src/store/createTransactionFlowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ interface TransactionFlowActions {
/** Set the auth mode for the validate step. */
setValidateAuthMode: (mode: AuthModeType) => void;

/** Store the validated transaction XDR. */
setValidatedXdr: (xdr: string) => void;
/** Store (or clear) the validated transaction XDR. */
setValidatedXdr: (xdr: string | undefined) => void;

/** Update fee bump params. */
setFeeBumpParams: (params: FeeBumpParamsObj) => void;
Expand Down
23 changes: 18 additions & 5 deletions tests/e2e/sorobanAuthEntry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ const seedSessionStorageAndNavigate = async (page: Page) => {
await page.evaluate(
(stateJson) => {
sessionStorage.setItem("stellar_lab_tx_flow_build", stateJson);

// Seed localStorage with testnet so the NetworkSelector doesn't open
// a "Load Network" modal on reload (CI has no saved network).
localStorage.setItem(
"stellar_lab_network",
JSON.stringify({
id: "testnet",
label: "Testnet",
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
passphrase: "Test SDF Network ; September 2015",
}),
);
},
JSON.stringify(storeState),
);
Expand Down Expand Up @@ -158,7 +171,7 @@ test.describe("Soroban Auth Entry Detection and Signing", () => {
await mockSimulateWithAuthEntries(page);
await seedSessionStorageAndNavigate(page);

const simulateButton = page.getByRole("button", { name: "Simulate" });
const simulateButton = page.getByRole("button", { name: "Simulate", exact: true });
await simulateButton.click();

// Wait for simulation result
Expand All @@ -184,7 +197,7 @@ test.describe("Soroban Auth Entry Detection and Signing", () => {
await mockSimulateWithoutAuthEntries(page);
await seedSessionStorageAndNavigate(page);

const simulateButton = page.getByRole("button", { name: "Simulate" });
const simulateButton = page.getByRole("button", { name: "Simulate", exact: true });
await simulateButton.click();

// Wait for simulation result
Expand All @@ -208,7 +221,7 @@ test.describe("Soroban Auth Entry Detection and Signing", () => {
await mockSimulateWithAuthEntries(page);
await seedSessionStorageAndNavigate(page);

const simulateButton = page.getByRole("button", { name: "Simulate" });
const simulateButton = page.getByRole("button", { name: "Simulate", exact: true });
await simulateButton.click();

await expect(
Expand All @@ -235,7 +248,7 @@ test.describe("Soroban Auth Entry Detection and Signing", () => {
await mockSimulateWithAuthEntries(page);
await seedSessionStorageAndNavigate(page);

const simulateButton = page.getByRole("button", { name: "Simulate" });
const simulateButton = page.getByRole("button", { name: "Simulate", exact: true });
await simulateButton.click();

await expect(
Expand All @@ -258,7 +271,7 @@ test.describe("Soroban Auth Entry Detection and Signing", () => {
await mockSimulateWithAuthEntries(page);
await seedSessionStorageAndNavigate(page);

const simulateButton = page.getByRole("button", { name: "Simulate" });
const simulateButton = page.getByRole("button", { name: "Simulate", exact: true });
await simulateButton.click();

await expect(
Expand Down
23 changes: 18 additions & 5 deletions tests/e2e/validateStepContent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ test.describe("Validate Step in Build Flow", () => {
// Seed sessionStorage with flow store state at the validate step
await page.evaluate((stateJson) => {
sessionStorage.setItem("stellar_lab_tx_flow_build", stateJson);

// Seed localStorage with testnet so the NetworkSelector doesn't open
// a "Load Network" modal on reload (CI has no saved network).
localStorage.setItem(
"stellar_lab_network",
JSON.stringify({
id: "testnet",
label: "Testnet",
horizonUrl: "https://horizon-testnet.stellar.org",
rpcUrl: "https://soroban-testnet.stellar.org",
passphrase: "Test SDF Network ; September 2015",
}),
);
}, JSON.stringify(storeState));

// Reload to pick up the seeded sessionStorage
Expand Down Expand Up @@ -149,7 +162,7 @@ test.describe("Validate Step in Build Flow", () => {
}) => {
await seedSessionStorageAndNavigate(page);

const validateButton = page.getByRole("button", { name: "Validate" });
const validateButton = page.getByRole("button", { name: "Validate", exact: true });
await expect(validateButton).toBeEnabled();
});

Expand All @@ -159,7 +172,7 @@ test.describe("Validate Step in Build Flow", () => {
await mockEnforceSimulationSuccess(page);
await seedSessionStorageAndNavigate(page);

const validateButton = page.getByRole("button", { name: "Validate" });
const validateButton = page.getByRole("button", { name: "Validate", exact: true });
await validateButton.click();

// Wait for success alert
Expand All @@ -179,7 +192,7 @@ test.describe("Validate Step in Build Flow", () => {
await mockEnforceSimulationFailure(page);
await seedSessionStorageAndNavigate(page);

const validateButton = page.getByRole("button", { name: "Validate" });
const validateButton = page.getByRole("button", { name: "Validate", exact: true });
await validateButton.click();

await expect(page.getByText("Validation failed")).toBeVisible();
Expand All @@ -191,7 +204,7 @@ test.describe("Validate Step in Build Flow", () => {
await mockEnforceSimulationSuccess(page);
await seedSessionStorageAndNavigate(page);

const validateButton = page.getByRole("button", { name: "Validate" });
const validateButton = page.getByRole("button", { name: "Validate", exact: true });
await validateButton.click();

await expect(
Expand Down Expand Up @@ -234,7 +247,7 @@ test.describe("Validate Step in Build Flow", () => {
await mockEnforceSimulationSuccess(page);
await seedSessionStorageAndNavigate(page);

const validateButton = page.getByRole("button", { name: "Validate" });
const validateButton = page.getByRole("button", { name: "Validate", exact: true });
await validateButton.click();

// Wait for success
Expand Down
Loading