Skip to content

Commit

Permalink
[Submit Tx] Add a Submit Tx Page (#879)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeesunikim committed Jun 18, 2024
1 parent ae1c715 commit d45c33d
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 27 deletions.
16 changes: 3 additions & 13 deletions src/app/(sidebar)/transaction/build/components/TransactionXdr.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/(sidebar)/transaction/sign/components/Import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const Import = () => {
<Card>
<div className="SignTx__xdr">
<XdrPicker
id="sign-transaction-xdr"
id="sign-tx-xdr"
label="Import a transaction envelope in XDR format"
value={txXdr || ""}
error={txErrMsg}
Expand Down
75 changes: 75 additions & 0 deletions src/app/(sidebar)/transaction/submit/components/ErrorResponse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
AccountRequiresMemoError,
BadResponseError,
NetworkError,
} from "@stellar/stellar-sdk";

import { Box } from "@/components/layout/Box";
import { TxResponse } from "@/components/TxResponse";
import { ValidationResponseCard } from "@/components/ValidationResponseCard";

interface ErrorProps {
error: NetworkError & {
response: {
data?: {
extras?: {
result_codes?: string;
result_xdr?: string;
};
};
};
};
}

export const ErrorResponse = ({ error }: ErrorProps) => {
let message = "",
extras = null;
if (error instanceof AccountRequiresMemoError) {
message = "This destination requires a memo.";
extras = (
<Box gap="xs">
<TxResponse label="Destination account:" value={error.accountId} />
<TxResponse label="Operation index:" value={error.operationIndex} />
</Box>
);
} 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 = (
<Box gap="xs">
<TxResponse
label="extras.result_codes:"
value={JSON.stringify(result_codes)}
/>

<TxResponse label="Result XDR:" value={result_xdr} />
</Box>
);
} else {
message =
error instanceof BadResponseError
? "Received a bad response when submitting."
: "An unknown error occurred.";
extras = (
<Box gap="xs">
<TxResponse
label="original error:"
value={JSON.stringify(error, null, 2)}
/>
</Box>
);
}

return (
<ValidationResponseCard
variant="error"
title="Transaction failed!"
subtitle={message}
response={extras}
/>
);
};
160 changes: 159 additions & 1 deletion src/app/(sidebar)/transaction/submit/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <div>Submit Transaction</div>;
const { network, xdr } = useStore();
const { blob, updateXdrBlob } = xdr;

const [txErr, setTxErr] = useState<any | null>(null);
const [txResponse, setTxResponse] = useState<TransactionResponse | null>(
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 (
<Box gap="md">
<div className="PageHeader">
<Text size="md" as="h1" weight="medium">
Submit Transaction
</Text>
</div>
<Card>
<Box gap="md">
<XdrPicker
id="submit-tx-xdr"
label="Input a base-64 encoded TransactionEnvelope:"
value={blob}
error={xdrJson?.error || ""}
onChange={(e) => {
updateXdrBlob(e.target.value);
}}
note="Enter a base-64 encoded XDR blob to decode."
hasCopyButton
/>

<div className="SignTx__CTA">
<Button
disabled={!blob || Boolean(xdrJson?.error)}
isLoading={submitTx.status === "pending"}
size="md"
variant={"secondary"}
onClick={onSubmit}
>
Submit transaction
</Button>
</div>

<Box gap="lg" direction="row" align="center" justify="end">
<div>
{xdrJson?.jsonString ? (
<div className="Tabs">
<div className="Tab" data-is-active="true">
JSON
</div>
</div>
) : null}
</div>
</Box>

<>
{xdrJson?.jsonString ? (
<div className="PageBody__content PageBody__scrollable">
<PrettyJson json={JSON.parse(xdrJson.jsonString)} />
</div>
) : null}
</>
</Box>
</Card>
<>
{submitTx.status === "success" && txResponse ? (
<ValidationResponseCard
variant="success"
title="Transaction submitted!"
subtitle={`Transaction succeeded with ${txResponse.operation_count} operation(s)`}
response={
<Box gap="xs">
<TxResponse label="Hash:" value={txResponse.hash} />
<TxResponse label="Ledger number:" value={txResponse.ledger} />
<TxResponse
label="Paging token:"
value={txResponse.paging_token}
/>
<TxResponse label="Result XDR:" value={txResponse.result_xdr} />
<TxResponse
label="Result Meta XDR:"
value={txResponse.result_meta_xdr}
/>
<TxResponse
label="Fee Meta XDR:"
value={txResponse.fee_meta_xdr}
/>
</Box>
}
/>
) : null}
</>
<>
{submitTx.status === "error" && txErr ? (
<ErrorResponse error={txErr} />
) : null}
</>
</Box>
);
}
16 changes: 4 additions & 12 deletions src/app/(sidebar)/xdr/view/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("json");

const isXdrInit = useIsXdrInit();
const {
data: latestTxn,
error: latestTxnError,
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
16 changes: 16 additions & 0 deletions src/components/TxResponse/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "./styles.scss";

import { Box } from "@/components/layout/Box";

export const TxResponse = ({
label,
value,
}: {
label: string;
value: string | number;
}) => (
<Box gap="xs">
<div>{label}</div>
<div className="TxResponse__value">{value}</div>
</Box>
);
7 changes: 7 additions & 0 deletions src/components/TxResponse/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@use "../../styles/utils.scss" as *;

.TxResponse {
&__value {
margin-left: var(--sds-gap-sm);
}
}
18 changes: 18 additions & 0 deletions src/hooks/useIsXdrInit.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit d45c33d

Please sign in to comment.