Skip to content

Commit

Permalink
feat: import using url query params
Browse files Browse the repository at this point in the history
  • Loading branch information
sohrab- committed Aug 4, 2022
1 parent eaf6036 commit d9fb262
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 47 deletions.
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Transaction } from "./components/client/Transaction";
import { Web3Provider } from "./components/common/Web3Provider";
import { Footer } from "./components/Footer";
import { Header } from "./components/header/Header";
import { ImportModal } from "./components/ImportModal";
import { Options } from "./components/options/Options";
import { Palette } from "./components/palette/Palette";
import { useSessionStoreWithoutUndo } from "./hooks/useSessionStore";
Expand Down Expand Up @@ -69,6 +70,7 @@ export const App: React.FC = () => {
</Flex>

<Options />
<ImportModal />
</Web3Provider>
</ChakraProvider>
);
Expand Down
90 changes: 90 additions & 0 deletions src/components/ImportModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
Alert,
AlertIcon,
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Skeleton,
Text,
} from "@chakra-ui/react";
import { useImport } from "../hooks/useImport";
import {
useSessionStoreWithoutUndo,
useSessionStoreWithUndo,
} from "../hooks/useSessionStore";
import { mapITransactionPreviewToITransaction } from "../models/preview-mappers";
import { DEFAULT_IMPORT } from "../models/state-default";
import { TransactionPreview } from "./palette/preview/TransactionPreview";

export const ImportModal: React.FC = () => {
const [isLoading, transaction, setImport] = useSessionStoreWithoutUndo(
(state) => [state.import.isLoading, state.import.transaction, state.set]
);
const setTransaction = useSessionStoreWithUndo((state) => state.set);

useImport(); // reads the query params

const onConfirm = () => {
setTransaction((state) => {
state.transaction = mapITransactionPreviewToITransaction(transaction!);
});
onClose();
};

const onClose = () => {
setImport((state) => {
state.import = DEFAULT_IMPORT;
});
};

return (
<Modal
size="lg"
closeOnOverlayClick={false}
isOpen={transaction !== undefined || isLoading}
onClose={onClose}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Import Transaction</ModalHeader>

<ModalCloseButton />

<ModalBody>
<Alert variant="left-accent" status="warning" mb="3">
<AlertIcon />
Only import and run transactions from trusted sources!
</Alert>

<Text mb="5">You are about to import the following transaction:</Text>

{transaction === undefined ? (
<Skeleton h="200px" />
) : (
<TransactionPreview
transaction={transaction!}
interactive={false}
/>
)}
</ModalBody>

<ModalFooter>
<Button
colorScheme="teal"
mr="2"
isDisabled={transaction === undefined}
onClick={onConfirm}
>
Confirm
</Button>
<Button onClick={onClose}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
};
1 change: 0 additions & 1 deletion src/components/palette/ImportTransaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const IMPORT_TYPES: Record<PreviewSource, string> = {

export const ImportTransaction: React.FC = () => {
const [importType, setImportType] = useState<PreviewSource>("tx");
const [importValue, setImportValue] = useState("");
const [transaction, setTransaction] = useState<ITransactionPreview>();
const [error, setError] = useState("");

Expand Down
39 changes: 21 additions & 18 deletions src/components/palette/preview/InstructionPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { AccountSummary } from "./AccountSummary";
export const InstructionPreview: React.FC<{
index: number;
instruction: IInstructionPreview;
}> = ({ instruction, index }) => {
interactive?: boolean;
}> = ({ instruction, index, interactive = true }) => {
const set = useSessionStoreWithUndo((state) => state.set);

const { name, programId, accountSummary, innerInstructions } = instruction;
Expand Down Expand Up @@ -50,23 +51,25 @@ export const InstructionPreview: React.FC<{

<Spacer />

<Tooltip label="Add to transaction">
<IconButton
mt="-1"
size="xs"
variant="ghost"
aria-label="Add to transaction"
icon={<AddIcon />}
onClick={() => {
set((state) => {
addTo(
state.transaction.instructions,
mapIInstructionPreviewToIInstruction(instruction)
);
});
}}
/>
</Tooltip>
{interactive && (
<Tooltip label="Add to transaction">
<IconButton
mt="-1"
size="xs"
variant="ghost"
aria-label="Add to transaction"
icon={<AddIcon />}
onClick={() => {
set((state) => {
addTo(
state.transaction.instructions,
mapIInstructionPreviewToIInstruction(instruction)
);
});
}}
/>
</Tooltip>
)}
</Flex>

<Flex ml="10" mt="1" alignItems="center">
Expand Down
42 changes: 20 additions & 22 deletions src/components/palette/preview/TransactionPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,36 +17,31 @@ import {
useColorModeValue,
} from "@chakra-ui/react";
import { useSessionStoreWithUndo } from "../../../hooks/useSessionStore";
import { mapIInstructionPreviewToIInstruction } from "../../../models/preview-mappers";
import { mapITransactionPreviewToITransaction } from "../../../models/preview-mappers";
import { ITransactionPreview } from "../../../models/preview-types";
import { addTo } from "../../../models/sortable";
import { short } from "../../../models/web3js-mappers";
import { CopyButton } from "../../common/CopyButton";
import { AccountSummary } from "./AccountSummary";
import { InstructionPreview } from "./InstructionPreview";

export const TransactionPreview: React.FC<{
transaction: ITransactionPreview;
}> = ({
transaction: {
interactive?: boolean;
}> = ({ transaction, interactive = true }) => {
const {
source,
sourceValue,
name,
rpcEndpoint,
instructions,
accountSummary,
error,
},
}) => {
const addInstructions = useSessionStoreWithUndo(
} = transaction;

const setTransaction = useSessionStoreWithUndo(
(state) => () =>
state.set((state) => {
instructions.forEach((instruction) => {
addTo(
state.transaction.instructions,
mapIInstructionPreviewToIInstruction(instruction)
);
});
state.transaction = mapITransactionPreviewToITransaction(transaction);
})
);

Expand Down Expand Up @@ -108,15 +103,17 @@ export const TransactionPreview: React.FC<{

<Spacer />

<Tooltip label="Add all instructions to transaction">
<IconButton
size="xs"
variant="ghost"
aria-label="Add all instructions to transaction"
icon={<AddIcon />}
onClick={addInstructions}
/>
</Tooltip>
{interactive && (
<Tooltip label="Add all instructions to transaction">
<IconButton
size="xs"
variant="ghost"
aria-label="Add all instructions to transaction"
icon={<AddIcon />}
onClick={setTransaction}
/>
</Tooltip>
)}
</Flex>

{accountSummary && (
Expand All @@ -128,6 +125,7 @@ export const TransactionPreview: React.FC<{
key={index}
index={index}
instruction={instruction}
interactive={interactive}
/>
))}
</Grid>
Expand Down
126 changes: 126 additions & 0 deletions src/hooks/useImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useToast } from "@chakra-ui/react";
import Ajv from "ajv";
import axios from "axios";
import { useEffect, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
import { JSON_SCHEMA } from "../models/external-types";
import {
mapITransactionExtToITransactionPreview,
mapTransactionResponseToITransactionPreview,
} from "../models/preview-mappers";
import { ITransactionPreview } from "../models/preview-types";
import { useGetWeb3Transaction } from "./useGetWeb3Transaction";
import { usePersistentStore } from "./usePersistentStore";
import { useSessionStoreWithoutUndo } from "./useSessionStore";

export const useImport = () => {
const [set, setIsLoading] = useSessionStoreWithoutUndo((state) => [
state.set,
(value: boolean) => {
state.set((state) => {
state.import.isLoading = value;
});
},
]);
const [searchParams, setSearchParams] = useSearchParams();

const toast = useToast();

// determine network
const network = searchParams.get("network");
const rpcEndpoints = usePersistentStore(
(state) => state.appOptions.rpcEndpoints
);
const rpcEndpoint = Object.values(rpcEndpoints.map).find(
(endpoint) => endpoint.network === (network || "mainnet-beta")
)!;

const { start } = useGetWeb3Transaction({
rpcEndpointUrl: rpcEndpoint.url,
onFinalised: (response) => {
set((state) => {
state.import = {
isLoading: false,
transaction: mapTransactionResponseToITransactionPreview(
response,
rpcEndpoint
),
};
});
},
onError: (error) => {
setIsLoading(false);
toast({
title: "Transaction import failed",
description: `Failed to fetch transcation: ${error.message}`,
status: "error",
duration: 15000,
isClosable: true,
});
},
});

const validate = useMemo(() => new Ajv().compile(JSON_SCHEMA), []);

// TODO load the transaction in the preview sidebar
// const preview = searchParams.has("preview");

const tx = searchParams.get("tx");
const share = searchParams.get("share");

// import from transaction ID
useEffect(() => {
if (!tx) return;

setIsLoading(true);
setSearchParams({});

start(tx, true);
}, [tx, setIsLoading, start, setSearchParams]);

// import from share URL
useEffect(() => {
if (!share) return;

setIsLoading(true);
setSearchParams({});

axios
.get(share)
.then((response) => {
if (!validate(response.data)) {
setIsLoading(false);
// TODO why is this triggering 3 times?
toast({
title: "Transaction import failed",
description: `Invalid JSON from URL`,
status: "error",
duration: 15000,
isClosable: true,
});
return;
}

set((state) => {
state.import = {
isLoading: false,
transaction: mapITransactionExtToITransactionPreview(
response.data as ITransactionPreview,
"shareUrl",
share
),
};
});
})
.catch((err) => {
setIsLoading(false);
toast({
title: "Transaction import failed",
description: `Cannot fetch tranasction from URL: ${err}`,
status: "error",
duration: 15000,
isClosable: true,
});
});
}, [share, set, setIsLoading, setSearchParams, toast, validate]);
};
5 changes: 4 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ColorModeScript } from "@chakra-ui/react";
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./App";
import reportWebVitals from "./reportWebVitals";
import * as serviceWorker from "./serviceWorker";
Expand All @@ -19,7 +20,9 @@ root.render(
}
</>
<ColorModeScript initialColorMode="system" />
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);

Expand Down
Loading

0 comments on commit d9fb262

Please sign in to comment.