Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
104 changes: 84 additions & 20 deletions src/app/(sidebar)/transaction/build/components/SimulateStepContent.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"use client";

import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Card, Icon, Link, Input } from "@stellar/design-system";
import {
Alert,
Button,
Card,
Icon,
Link,
Input,
} from "@stellar/design-system";
TransactionBuilder,
xdr,
rpc as StellarRpc,
} from "@stellar/stellar-sdk";

import { useBuildFlowStore } from "@/store/createTransactionFlowStore";
import { useStore } from "@/store/useStore";
Expand Down Expand Up @@ -61,17 +59,73 @@ export const SimulateStepContent = () => {
setSimulationReadOnly,
setAuthEntriesXdr,
setSignedAuthEntriesXdr,
setAssembledXdr,
} = useBuildFlowStore();

const [instrLeewayError, setInstrLeewayError] = 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);

// Derive the built XDR from whichever operation type was used
const builtXdr = build.soroban.xdr || build.classic.xdr;

/**
* After all auth entries are signed, assemble the transaction with the
* signed auth entries and store the assembled XDR for the Sign step.
*/
const assembleWithSignedAuth = useCallback(
(signedEntries: string[]) => {
if (!simulate.simulationResultJson || !builtXdr || !network.passphrase) {
return;
}

try {
// Parse the original transaction
const rawTx = TransactionBuilder.fromXDR(builtXdr, network.passphrase);

// Parse the stored simulation result
const rawResponse = JSON.parse(simulate.simulationResultJson);
const parsedSim = StellarRpc.parseRawSimulation(rawResponse.result);

// Assemble with resources and fees from simulation
const assembled = StellarRpc.assembleTransaction(
rawTx,
parsedSim,
).build();

// Replace auth entries on the assembled transaction with signed versions
const envelope = assembled.toEnvelope();
const ops = envelope.v1().tx().operations();

for (const op of ops) {
if (op.body().switch() === xdr.OperationType.invokeHostFunction()) {
const ihf = op.body().invokeHostFunctionOp();
const signedAuth = signedEntries.map((entryBase64) =>
xdr.SorobanAuthorizationEntry.fromXDR(entryBase64, "base64"),
);
ihf.auth(signedAuth);
}
}

const finalXdr = envelope.toXDR("base64");
setAssembledXdr(finalXdr);
setSignedAuthEntriesXdr(signedEntries);
} catch (e) {
console.error("Assembly with signed auth entries failed:", e);
}
},
[
simulate.simulationResultJson,
builtXdr,
network.passphrase,
setAssembledXdr,
setSignedAuthEntriesXdr,
],
);

const {
mutateAsync: simulateTx,
data: simulateTxData,
Expand Down Expand Up @@ -111,15 +165,15 @@ export const SimulateStepContent = () => {
transactionXdr: builtXdr,
headers: getNetworkHeaders(network, "rpc"),
xdrFormat: "json",
// authMode: simulate.authMode,
authMode: simulate.authMode,
})
: null,
simulateTx({
rpcUrl: network.rpcUrl,
transactionXdr: builtXdr,
headers: getNetworkHeaders(network, "rpc"),
xdrFormat: "base64",
// authMode: simulate.authMode,
authMode: simulate.authMode,
}),
]);

Expand All @@ -141,8 +195,18 @@ export const SimulateStepContent = () => {
const isReadOnly = checkIsReadOnly(simBase64Response);
setSimulationReadOnly(isReadOnly);

// Compute validUntilLedgerSeq from latestLedger in response
const latestLedger = Number(
simBase64Response?.result?.latestLedger ?? 0,
);
if (latestLedger > 0) {
// ~42 minutes buffer at 5 seconds per ledger
setValidUntilLedgerSeq(latestLedger + 500);
}

// Extract and store auth entries
const entries = extractAuthEntries(simBase64Response);

if (entries.length > 0) {
setAuthEntriesXdr(entries);
}
Expand All @@ -158,7 +222,7 @@ export const SimulateStepContent = () => {
simulateTxError || simulateTxData?.error || simulateTxData?.result?.error,
);
const isSimulationSuccess = hasSimulationResult && !hasError;
const authEntries = simulateTxData ? extractAuthEntries(simulateTxData) : [];
const authEntries = simulate.authEntriesXdr || [];
const hasAuthEntries = authEntries.length > 0;
const resourceInfo = getSimulationResourceInfo(simulateTxData);

Expand Down Expand Up @@ -203,7 +267,6 @@ export const SimulateStepContent = () => {
<XdrFormat
selectedFormat={xdrFormat || "json"}
onChange={(format) => {
console.log({ format });
setXdrFormat(format);

if (hasSimulationResult) {
Expand Down Expand Up @@ -311,14 +374,15 @@ export const SimulateStepContent = () => {
authEntriesXdr={authEntries}
signedAuthEntriesXdr={simulate.signedAuthEntriesXdr || []}
builtXdr={builtXdr}
onAuthSigned={({ signedXdr }) => {
// TODO: Replace with authorizeEntry() logic — SignTransactionXdr
// signs the transaction envelope, but auth entries need
// authorizeEntry() from @stellar/stellar-sdk to sign each
// SorobanAuthorizationEntry individually.
if (signedXdr) {
setSignedAuthEntriesXdr(authEntries);
}
validUntilLedgerSeq={validUntilLedgerSeq}
networkPassphrase={network.passphrase}
onAuthEntrySigned={(index, signedEntryXdr) => {
const updated = [...(simulate.signedAuthEntriesXdr || [])];
updated[index] = signedEntryXdr;
setSignedAuthEntriesXdr(updated);
}}
onAllEntriesSigned={(signedEntries) => {
assembleWithSignedAuth(signedEntries);
}}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@ import { PageHeader } from "@/components/layout/PageHeader";
import { CodeEditor } from "@/components/CodeEditor";
import { ExpandBox } from "@/components/ExpandBox";
import { SdsLink } from "@/components/SdsLink";
import { SorobanAuthSigningCard } from "@/components/SorobanAuthSigning";

import { getNetworkHeaders } from "@/helpers/getNetworkHeaders";
import { extractAuthEntries } from "@/helpers/sorobanAuthUtils";

import { trackEvent, TrackingEvent } from "@/metrics/tracking";

Expand All @@ -50,14 +47,11 @@ import {
export const ValidateStepContent = () => {
const { network } = useStore();
const {
build,
simulate,
sign,
validate,
setValidateResult,
setValidateAuthMode,
setValidatedXdr,
setSignedAuthEntriesXdr,
} = useBuildFlowStore();

const [isResourcesExpanded, setIsResourcesExpanded] = useState(false);
Expand Down Expand Up @@ -120,9 +114,7 @@ export const ValidateStepContent = () => {
);
const isValidationSuccess = hasValidationResult && !hasError;
const resourceInfo = getSimulationResourceInfo(simulateTxData);
const authEntries = simulateTxData ? extractAuthEntries(simulateTxData) : [];
const hasAuthEntries = authEntries.length > 0;
const builtXdr = build.soroban.xdr || build.classic.xdr;


return (
<Box gap="md">
Expand Down Expand Up @@ -237,19 +229,6 @@ export const ValidateStepContent = () => {
</Box>
)}

{/* Auth entry signing card */}
{hasAuthEntries && (
<SorobanAuthSigningCard
authEntriesXdr={authEntries}
signedAuthEntriesXdr={simulate.signedAuthEntriesXdr || []}
builtXdr={builtXdr}
onAuthSigned={({ signedXdr: signed }) => {
if (signed) {
setSignedAuthEntriesXdr(authEntries);
}
}}
/>
)}
</Box>
</Card>
)}
Expand Down
50 changes: 50 additions & 0 deletions src/components/SignTransactionXdr/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const SignTransactionXdr = ({
isDisabled = false,
description,
customFooter,
customSignFn,
}: {
id: string;
title: string;
Expand All @@ -69,6 +70,18 @@ export const SignTransactionXdr = ({
isDisabled?: boolean;
description?: string;
customFooter?: React.ReactNode;
/**
* Optional custom signing function. When provided, replaces the default
* envelope signing logic. Used for Soroban auth entry signing where
* `authorizeEntry()` is needed instead of `tx.sign()`.
*
* Receives the secret key inputs and should return success/error messages.
* The component manages its own UI state (tabs, messages) around this.
*/
customSignFn?: (params: {
sigType: TxSignatureType;
secretKeys: string[];
}) => Promise<{ successMessage: string; errorMessage: string }>;
}) => {
const { network, walletKit } = useStore();

Expand Down Expand Up @@ -171,6 +184,42 @@ export const SignTransactionXdr = ({
return;
}

// Custom sign mode: delegate signing to external handler
if (customSignFn && !isClear) {
try {
const result = await customSignFn({
sigType,
secretKeys: secretKeyInputs,
});

if (result.successMessage) {
setSecretKeySuccessMsg(result.successMessage);
setSecretKeyErrorMsg("");
setAllSigsCount((prev) => ({ ...prev, secretKey: 1 }));
}

if (result.errorMessage) {
setSecretKeyErrorMsg(result.errorMessage);
setSecretKeySuccessMsg("");
}

onDoneAction({
signedXdr: null,
successMessage: result.successMessage || null,
errorMessage: result.errorMessage || null,
});
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
setSecretKeyErrorMsg(msg);
onDoneAction({
signedXdr: null,
successMessage: null,
errorMessage: msg,
});
}
return;
}

onDoneAction({
signedXdr: null,
successMessage: null,
Expand Down Expand Up @@ -724,6 +773,7 @@ export const SignTransactionXdr = ({
autocomplete="off"
isPassword
useSecretSelector
limit={customSignFn ? 1 : undefined}
submitButton={
<SignTxButton
onSign={async () => {
Expand Down
Loading
Loading