diff --git a/src/app/(sidebar)/transaction/submit/page.tsx b/src/app/(sidebar)/transaction/submit/page.tsx index b94c80d4..51f8bf88 100644 --- a/src/app/(sidebar)/transaction/submit/page.tsx +++ b/src/app/(sidebar)/transaction/submit/page.tsx @@ -67,6 +67,47 @@ const SUBMIT_OPTIONS = [ }, ]; +// Define operation types for better type safety +interface DecodedOperationBody { + extend_footprint_ttl?: { ext: string }; + restore_footprint?: { ext: string }; + invoke_host_function?: { ext: string }; +} + +interface DecodedOperation { + body: DecodedOperationBody; +} + +// Move function outside component to prevent recreation +const isSorobanXdr = (jsonString: string): boolean => { + try { + const parsedXdr = JSON.parse(jsonString); + const operations = parsedXdr?.tx?.tx?.operations; + + if (!Array.isArray(operations)) { + return false; + } + + return operations.some((op: DecodedOperation) => { + const body = op?.body; + if (!body) { + return false; + } + + const sorobanOps = [ + body.extend_footprint_ttl, + body.restore_footprint, + body.invoke_host_function, + ]; + + return sorobanOps.some((op) => op?.ext === "v0"); + }); + } catch (e) { + console.debug("Error parsing XDR:", e); + return false; + } +}; + export default function SubmitTransaction() { const { network, xdr, transaction } = useStore(); const { blob, updateXdrBlob } = xdr; @@ -80,6 +121,7 @@ export default function SubmitTransaction() { const [isSaveTxnModalVisible, setIsSaveTxnModalVisible] = useState(false); const [isDropdownActive, setIsDropdownActive] = useState(false); const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [isSorobanTx, setIsSorobanTx] = useState(false); const [submitMethod, setSubmitMethod] = useState<"horizon" | "rpc" | string>( "", ); @@ -129,7 +171,9 @@ export default function SubmitTransaction() { useEffect(() => { const localStorageMethod = localStorageSettings.get(SETTINGS_SUBMIT_METHOD); - if (localStorageMethod) { + if (isSorobanTx && isRpcAvailable) { + setSubmitMethod("rpc"); + } else if (localStorageMethod) { setSubmitMethod(localStorageMethod); } else { setSubmitMethod(isRpcAvailable ? "rpc" : "horizon"); @@ -138,7 +182,7 @@ export default function SubmitTransaction() { resetSubmitState(); // Not including resetSubmitState // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isRpcAvailable]); + }, [isRpcAvailable, isSorobanTx]); // Scroll to response useScrollIntoView(isSuccess, responseSuccessEl); @@ -497,6 +541,13 @@ export default function SubmitTransaction() { return null; }; + useEffect(() => { + if (xdrJson?.jsonString) { + const hasSorobanOp = isSorobanXdr(xdrJson.jsonString); + setIsSorobanTx(hasSorobanOp); + } + }, [xdrJson?.jsonString]); + return ( diff --git a/tests/submitTransactionPage.test.ts b/tests/submitTransactionPage.test.ts index afcb2c65..73a3245c 100644 --- a/tests/submitTransactionPage.test.ts +++ b/tests/submitTransactionPage.test.ts @@ -416,6 +416,25 @@ test.describe("Submit Transaction Page", () => { // Omitting the API end result because the test gives inconsistenet results }); }); + + test.describe("Update default submit method to RPC when it is a Soroban XDR", () => { + test("Submit Soroban", async ({ page }) => { + const xdrInput = page.getByLabel( + "Input a base-64 encoded TransactionEnvelope", + ); + const submitMethodsBtn = page + .locator(".SubmitTx__buttons") + .getByRole("button", { name: /via/i }); + + await expect(submitMethodsBtn).toBeVisible(); + + // Input the Soroban XDR + await xdrInput.fill(MOCK_VALID_SOROBAN_TX_XDR.XDR); + + // Check if the submit method button shows RPC as default + await expect(submitMethodsBtn).toHaveText("via RPC"); + }); + }); }); const testSuccessHashAndJson = async ({ @@ -538,6 +557,12 @@ const MOCK_VALID_SUCCESS_TX_XDR_RPC = { hash: "00cb774dce521a93438236d49f8154ede32729a595c797d51c1a72c364056fd0", }; +const MOCK_VALID_SOROBAN_TX_XDR = { + XDR: "AAAAAgAAAAB+TL0HLiAjanMRnyeqyhb8Iu+4d1g2dl1cwPi1UZAigwAAtwUABiLjAAAAGQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGQAAAAAAAHUwAAAAAQAAAAAAAAABAAAABgAAAAEg/u86MzPrVcpNrsFUa84T82Kss8DLAE9ZMxLqhM22HwAAABAAAAABAAAAAgAAAA8AAAAHQ291bnRlcgAAAAASAAAAAAAAAAB+TL0HLiAjanMRnyeqyhb8Iu+4d1g2dl1cwPi1UZAigwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtqEAAAABUZAigwAAAEADYbntiznotYPblvJQ35DiGEpMTQU9jCYANxV18VVGV6zDFSjB+qK++dF656Pr4oMTpyBVvE15YSo6ITxR5DoE", + JSON: `{"tx":{"tx":{"source_account":"GB7EZPIHFYQCG2TTCGPSPKWKC36CF35YO5MDM5S5LTAPRNKRSARIHWGG","fee":46853,"seq_num":1727208213184537,"cond":{"time":{"min_time":0,"max_time":0}},"memo":"none","operations":[{"source_account":null,"body":{"extend_footprint_ttl":{"ext":"v0","extend_to":30000}}}],"ext":{"v1":{"ext":"v0","resources":{"footprint":{"read_only":[{"contract_data":{"contract":"CAQP53Z2GMZ6WVOKJWXMCVDLZYJ7GYVMWPAMWACPLEZRF2UEZW3B636S","key":{"vec":[{"symbol":"Counter"},{"address":"GB7EZPIHFYQCG2TTCGPSPKWKC36CF35YO5MDM5S5LTAPRNKRSARIHWGG"}]},"durability":"persistent"}}],"read_write":[]},"instructions":0,"read_bytes":0,"write_bytes":0},"resource_fee":46753}}},"signatures":[{"hint":"51902283","signature":"0361b9ed8b39e8b583db96f250df90e2184a4c4d053d8c2600371575f1554657acc31528c1faa2bef9d17ae7a3ebe28313a72055bc4d79612a3a213c51e43a04"}]}}`, + hash: "0571508f487a343006d231d11f201a855f67973e0e019da6164b32b992dab46f", +}; + const MOCK_VALID_TX_SUCCESS_HORIZON_RESPONSE = { _links: { self: {