diff --git a/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx b/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx index c88d11c3..3c97b004 100644 --- a/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx +++ b/src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx @@ -1,6 +1,5 @@ "use client"; -import { useEffect, useState } from "react"; import { Button } from "@stellar/design-system"; import { stringify } from "lossless-json"; import { StrKey, TransactionBuilder } from "@stellar/stellar-sdk"; @@ -15,6 +14,7 @@ import { Box } from "@/components/layout/Box"; import { isEmptyObject } from "@/helpers/isEmptyObject"; import { xdrUtils } from "@/helpers/xdr/utils"; import { optionsFlagDetails } from "@/helpers/optionsFlagDetails"; +import { useIsXdrInit } from "@/hooks/useIsXdrInit"; import { useStore } from "@/store/useStore"; import { Routes } from "@/constants/routes"; @@ -48,19 +48,9 @@ export const TransactionXdr = () => { } = transaction.build; const { updateSignActiveView, updateSignImportXdr } = transaction; - const [isReady, setIsReady] = useState(false); + const isXdrInit = useIsXdrInit(); - useEffect(() => { - // Stellar XDR init - const init = async () => { - await StellarXdr.init(); - setIsReady(true); - }; - - init(); - }, []); - - if (!(isReady && isValid.params && isValid.operations)) { + if (!(isXdrInit && isValid.params && isValid.operations)) { return null; } diff --git a/src/app/(sidebar)/transaction/sign/components/Import.tsx b/src/app/(sidebar)/transaction/sign/components/Import.tsx index 921407c4..e2ae9248 100644 --- a/src/app/(sidebar)/transaction/sign/components/Import.tsx +++ b/src/app/(sidebar)/transaction/sign/components/Import.tsx @@ -66,7 +66,7 @@ export const Import = () => {
{ + let message = "", + extras = null; + if (error instanceof AccountRequiresMemoError) { + message = "This destination requires a memo."; + extras = ( + + + + + ); + } else if ( + error?.response && + error.response.data?.extras?.result_codes && + error.response.data?.extras.result_xdr + ) { + const { result_codes, result_xdr } = error.response.data.extras; + message = error.message; + extras = ( + + + + + + ); + } else { + message = + error instanceof BadResponseError + ? "Received a bad response when submitting." + : "An unknown error occurred."; + extras = ( + + + + ); + } + + return ( + + ); +}; diff --git a/src/app/(sidebar)/transaction/submit/page.tsx b/src/app/(sidebar)/transaction/submit/page.tsx index 78b768b8..f97132d0 100644 --- a/src/app/(sidebar)/transaction/submit/page.tsx +++ b/src/app/(sidebar)/transaction/submit/page.tsx @@ -1,5 +1,163 @@ "use client"; +import { useState } from "react"; +import { Button, Card, Text } from "@stellar/design-system"; +import { Horizon, TransactionBuilder } from "@stellar/stellar-sdk"; + +import { useStore } from "@/store/useStore"; + +import * as StellarXdr from "@/helpers/StellarXdr"; + +import { useIsXdrInit } from "@/hooks/useIsXdrInit"; + +import { TransactionResponse, useSubmitTx } from "@/query/useSubmitTx"; + +import { Box } from "@/components/layout/Box"; +import { PrettyJson } from "@/components/PrettyJson"; +import { XdrPicker } from "@/components/FormElements/XdrPicker"; +import { ValidationResponseCard } from "@/components/ValidationResponseCard"; +import { TxResponse } from "@/components/TxResponse"; +import { ErrorResponse } from "./components/ErrorResponse"; + export default function SubmitTransaction() { - return
Submit Transaction
; + const { network, xdr } = useStore(); + const { blob, updateXdrBlob } = xdr; + + const [txErr, setTxErr] = useState(null); + const [txResponse, setTxResponse] = useState( + null, + ); + + const isXdrInit = useIsXdrInit(); + const submitTx = useSubmitTx(); + + const onSubmit = () => { + const transaction = TransactionBuilder.fromXDR(blob, network.passphrase); + + const server = new Horizon.Server(network.horizonUrl, { + appName: "Laboratory", + }); + + submitTx.mutate( + { transaction, server }, + { + onSuccess: (res) => setTxResponse(res), + onError: (res) => setTxErr(res), + }, + ); + }; + + const getXdrJson = () => { + const xdrType = "TransactionEnvelope"; + + if (!(isXdrInit && blob)) { + return null; + } + + try { + const xdrJson = StellarXdr.decode(xdrType, blob); + + return { + jsonString: xdrJson, + error: "", + }; + } catch (e) { + return { + jsonString: "", + error: `Unable to decode input as ${xdrType}`, + }; + } + }; + + const xdrJson = getXdrJson(); + + return ( + +
+ + Submit Transaction + +
+ + + { + updateXdrBlob(e.target.value); + }} + note="Enter a base-64 encoded XDR blob to decode." + hasCopyButton + /> + +
+ +
+ + +
+ {xdrJson?.jsonString ? ( +
+
+ JSON +
+
+ ) : null} +
+
+ + <> + {xdrJson?.jsonString ? ( +
+ +
+ ) : null} + +
+
+ <> + {submitTx.status === "success" && txResponse ? ( + + + + + + + +
+ } + /> + ) : null} + + <> + {submitTx.status === "error" && txErr ? ( + + ) : null} + + + ); } diff --git a/src/app/(sidebar)/xdr/view/page.tsx b/src/app/(sidebar)/xdr/view/page.tsx index 4a43d46a..4e026504 100644 --- a/src/app/(sidebar)/xdr/view/page.tsx +++ b/src/app/(sidebar)/xdr/view/page.tsx @@ -22,15 +22,17 @@ import { XdrPicker } from "@/components/FormElements/XdrPicker"; import { PrettyJson } from "@/components/PrettyJson"; import { Tabs } from "@/components/Tabs"; +import { useIsXdrInit } from "@/hooks/useIsXdrInit"; + import { useStore } from "@/store/useStore"; export default function ViewXdr() { const { xdr, network } = useStore(); const { updateXdrBlob, updateXdrType, resetXdr } = xdr; - const [isReady, setIsReady] = useState(false); const [activeTab, setActiveTab] = useState("json"); + const isXdrInit = useIsXdrInit(); const { data: latestTxn, error: latestTxnError, @@ -40,16 +42,6 @@ export default function ViewXdr() { refetch: fetchLatestTxn, } = useLatestTxn(network.horizonUrl); - useEffect(() => { - // Stellar XDR init - const init = async () => { - await StellarXdr.init(); - setIsReady(true); - }; - - init(); - }, []); - useEffect(() => { if (isLatestTxnSuccess && latestTxn) { updateXdrBlob(latestTxn); @@ -60,7 +52,7 @@ export default function ViewXdr() { const isFetchingLatestTxn = isLatestTxnFetching || isLatestTxnLoading; const xdrDecodeJson = () => { - if (!(isReady && xdr.blob && xdr.type)) { + if (!(isXdrInit && xdr.blob && xdr.type)) { return null; } diff --git a/src/components/TxResponse/index.tsx b/src/components/TxResponse/index.tsx new file mode 100644 index 00000000..34839f55 --- /dev/null +++ b/src/components/TxResponse/index.tsx @@ -0,0 +1,16 @@ +import "./styles.scss"; + +import { Box } from "@/components/layout/Box"; + +export const TxResponse = ({ + label, + value, +}: { + label: string; + value: string | number; +}) => ( + +
{label}
+
{value}
+
+); diff --git a/src/components/TxResponse/styles.scss b/src/components/TxResponse/styles.scss new file mode 100644 index 00000000..0b74fe5a --- /dev/null +++ b/src/components/TxResponse/styles.scss @@ -0,0 +1,7 @@ +@use "../../styles/utils.scss" as *; + +.TxResponse { + &__value { + margin-left: var(--sds-gap-sm); + } +} diff --git a/src/hooks/useIsXdrInit.ts b/src/hooks/useIsXdrInit.ts new file mode 100644 index 00000000..8bb63754 --- /dev/null +++ b/src/hooks/useIsXdrInit.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from "react"; +import * as StellarXdr from "@/helpers/StellarXdr"; + +export const useIsXdrInit = () => { + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + // Stellar XDR init + const init = async () => { + await StellarXdr.init(); + setIsReady(true); + }; + + init(); + }, []); + + return isReady; +}; diff --git a/src/query/useSubmitTx.ts b/src/query/useSubmitTx.ts new file mode 100644 index 00000000..60d0f627 --- /dev/null +++ b/src/query/useSubmitTx.ts @@ -0,0 +1,45 @@ +import { useMutation } from "@tanstack/react-query"; + +import { Horizon, FeeBumpTransaction, Transaction } from "@stellar/stellar-sdk"; +import { MemoType } from "@stellar/stellar-base"; + +export interface TransactionResponse + extends Horizon.HorizonApi.SubmitTransactionResponse, + Horizon.HorizonApi.BaseResponse< + "account" | "ledger" | "operations" | "effects" | "succeeds" | "precedes" + > { + created_at: string; + fee_meta_xdr: string; + fee_charged: number | string; + max_fee: number | string; + id: string; + memo_type: MemoType; + memo?: string; + memo_bytes?: string; + operation_count: number; + paging_token: string; + signatures: string[]; + source_account: string; + source_account_sequence: string; + fee_account: string; + inner_transaction?: Horizon.HorizonApi.InnerTransactionResponse; + fee_bump_transaction?: Horizon.HorizonApi.FeeBumpTransactionResponse; + preconditions?: Horizon.HorizonApi.TransactionPreconditions; +} + +export const useSubmitTx = () => { + return useMutation({ + mutationFn: async ({ + transaction, + server, + }: { + transaction: Transaction | FeeBumpTransaction; + server: Horizon.Server; + }) => { + const response = await server.submitTransaction(transaction); + return response as TransactionResponse; + }, + onSuccess: (response) => response, + onError: (err) => err, + }); +};