From e648d73871e2141a0317e887d8bcac51460083e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jeesun=20=EC=A7=80=EC=84=A0?= Date: Mon, 4 Nov 2024 08:22:03 -0800 Subject: [PATCH 1/9] display 'transaction hash' component only if the xdr type is tx (#1132) --- src/app/(sidebar)/xdr/view/page.tsx | 2 +- src/components/TransactionHashReadOnlyField.tsx | 8 +++++++- src/helpers/transactionHashFromXdr.ts | 13 ++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/app/(sidebar)/xdr/view/page.tsx b/src/app/(sidebar)/xdr/view/page.tsx index eb0ec768..c74eabcb 100644 --- a/src/app/(sidebar)/xdr/view/page.tsx +++ b/src/app/(sidebar)/xdr/view/page.tsx @@ -71,7 +71,7 @@ export default function ViewXdr() { } catch (e) { return { jsonString: "", - error: `Unable to decode input as ${xdr.type}: ${e}`, + error: `Unable to decode input as ${xdr.type}: ${e}. Select another XDR type.`, }; } }; diff --git a/src/components/TransactionHashReadOnlyField.tsx b/src/components/TransactionHashReadOnlyField.tsx index f0a70bf1..4293ff24 100644 --- a/src/components/TransactionHashReadOnlyField.tsx +++ b/src/components/TransactionHashReadOnlyField.tsx @@ -12,12 +12,18 @@ export const TransactionHashReadOnlyField = ({ return null; } + const hashValue = transactionHashFromXdr(xdr, networkPassphrase); + + if (!hashValue) { + return null; + } + return ( diff --git a/src/helpers/transactionHashFromXdr.ts b/src/helpers/transactionHashFromXdr.ts index 632601be..c56f9b6c 100644 --- a/src/helpers/transactionHashFromXdr.ts +++ b/src/helpers/transactionHashFromXdr.ts @@ -3,4 +3,15 @@ import { TransactionBuilder } from "@stellar/stellar-sdk"; export const transactionHashFromXdr = ( xdr: string, networkPassphrase: string, -) => TransactionBuilder.fromXDR(xdr, networkPassphrase).hash().toString("hex"); +) => { + try { + // Code to read XDR data + return TransactionBuilder?.fromXDR(xdr, networkPassphrase) + .hash() + .toString("hex"); + } catch (e) { + // @TODO Do nothing for now + // add amplitude error tracking + return; + } +}; From 1a1c7a8fa0668897ca00a2bd9bee2be84b507047 Mon Sep 17 00:00:00 2001 From: Iveta Date: Mon, 4 Nov 2024 13:21:53 -0500 Subject: [PATCH 2/9] Update Add Operation button + website title + undo auto add secret key field + secret key error message (#1127) * Update Add Operation button to tertiary variant * Update website title * Undo auto adding secret key fields * Update secret key error message --- .../transaction/build/components/Operations.tsx | 2 +- .../(sidebar)/transaction/sign/components/Overview.tsx | 2 -- src/app/layout.tsx | 3 +-- src/validate/methods/getSecretKeyError.ts | 10 ++-------- 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/app/(sidebar)/transaction/build/components/Operations.tsx b/src/app/(sidebar)/transaction/build/components/Operations.tsx index e7047a51..ca9725a6 100644 --- a/src/app/(sidebar)/transaction/build/components/Operations.tsx +++ b/src/app/(sidebar)/transaction/build/components/Operations.tsx @@ -1147,7 +1147,7 @@ export const Operations = () => { - } - footerRightEl={ -
- - - -
- } - /> + } + footerRightEl={ +
+ + + +
+ } + /> + ) : null} {signError ? ( diff --git a/src/helpers/scrollElIntoView.ts b/src/helpers/scrollElIntoView.ts new file mode 100644 index 00000000..5d94865f --- /dev/null +++ b/src/helpers/scrollElIntoView.ts @@ -0,0 +1,12 @@ +import { delayedAction } from "@/helpers/delayedAction"; + +export const scrollElIntoView = ( + scrollEl: React.MutableRefObject, +) => { + delayedAction({ + action: () => { + scrollEl?.current?.scrollIntoView({ behavior: "smooth" }); + }, + delay: 300, + }); +}; diff --git a/src/hooks/useScrollIntoView.ts b/src/hooks/useScrollIntoView.ts index 64ecd297..ada92b3c 100644 --- a/src/hooks/useScrollIntoView.ts +++ b/src/hooks/useScrollIntoView.ts @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { delayedAction } from "@/helpers/delayedAction"; +import { scrollElIntoView } from "@/helpers/scrollElIntoView"; export const useScrollIntoView = ( isEnabled: boolean, @@ -7,12 +7,7 @@ export const useScrollIntoView = ( ) => { useEffect(() => { if (isEnabled) { - delayedAction({ - action: () => { - scrollEl?.current?.scrollIntoView({ behavior: "smooth" }); - }, - delay: 300, - }); + scrollElIntoView(scrollEl); } }, [isEnabled, scrollEl]); }; From 51f9410f03069baf7fcdec2862c4d884da2d51f2 Mon Sep 17 00:00:00 2001 From: Iveta Date: Tue, 5 Nov 2024 08:26:06 -0500 Subject: [PATCH 4/9] Handle global errors + fix fetch sequence number crash (#1135) --- src/app/error.tsx | 60 +++++++++++++++++++++++++++ src/query/useAccountSequenceNumber.ts | 34 ++++++++------- 2 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 src/app/error.tsx diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 00000000..fbf2e961 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { Button, Card, Heading, Icon, Text } from "@stellar/design-system"; +import { Box } from "@/components/layout/Box"; +import { LayoutContentContainer } from "@/components/layout/LayoutContentContainer"; +import { openUrl } from "@/helpers/openUrl"; + +export default function Error({ + error, +}: { + error: Error & { digest?: string }; +}) { + // TODO: track this in Sentry once set up + console.log("Unhandled error: ", error); + + return ( + + + + + + Unhandled Error + + + + Uh-oh, we didn’t handle this error. We would appreciate it if you + opened an issue on GitHub, providing as many details as possible + to help us fix this bug. + + + + + + + + + + + + ); +} diff --git a/src/query/useAccountSequenceNumber.ts b/src/query/useAccountSequenceNumber.ts index fdd102f0..c7bba05c 100644 --- a/src/query/useAccountSequenceNumber.ts +++ b/src/query/useAccountSequenceNumber.ts @@ -18,26 +18,30 @@ export const useAccountSequenceNumber = ({ sourceAccount = muxedAccount.baseAccount().accountId(); } - const response = await fetch(`${horizonUrl}/accounts/${sourceAccount}`); - const responseJson = await response.json(); + try { + const response = await fetch(`${horizonUrl}/accounts/${sourceAccount}`); + const responseJson = await response.json(); - if (responseJson?.status === 0) { - throw `Unable to reach server at ${horizonUrl}.`; - } + if (responseJson?.status === 0) { + throw `Unable to reach server at ${horizonUrl}.`; + } - if (responseJson?.status?.toString()?.startsWith("4")) { - if (responseJson?.title === "Resource Missing") { - throw "Account not found. Make sure the correct network is selected and the account is funded/created."; + if (responseJson?.status?.toString()?.startsWith("4")) { + if (responseJson?.title === "Resource Missing") { + throw "Account not found. Make sure the correct network is selected and the account is funded/created."; + } + + throw ( + responseJson?.extras?.reason || + responseJson?.detail || + "Something went wrong when fetching the transaction sequence number. Please try again." + ); } - throw ( - responseJson?.extras?.reason || - responseJson?.detail || - "Something went wrong when fetching the transaction sequence number. Please try again." - ); + return (BigInt(responseJson.sequence) + BigInt(1)).toString(); + } catch (e: any) { + throw `${e}. Check network configuration.`; } - - return (BigInt(responseJson.sequence) + BigInt(1)).toString(); }, enabled: false, }); From 586af2bfdb727156837a681ae68dbd2c8b27db92 Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 6 Nov 2024 10:19:32 -0500 Subject: [PATCH 5/9] Network selector: setup for custom headers (#1101) * Network Selector UI updated * Update network requests * Fix tests * Comment out custom headers for now * Replace SorobanRpc with StellarRpc --- src/app/(sidebar)/account/create/page.tsx | 2 + src/app/(sidebar)/account/fund/page.tsx | 2 + .../(sidebar)/endpoints/[[...pages]]/page.tsx | 10 +- .../transaction/build/components/Params.tsx | 2 + .../(sidebar)/transaction/simulate/page.tsx | 2 + src/app/(sidebar)/transaction/submit/page.tsx | 44 +- src/app/(sidebar)/xdr/view/page.tsx | 3 +- .../FormElements/LedgerSeqPicker.tsx | 2 + src/components/NetworkSelector/index.tsx | 391 ++++++++++-------- src/components/PrettyJsonTransaction.tsx | 2 + src/helpers/fetchTxSignatures.ts | 7 +- src/helpers/getNetworkHeaders.ts | 16 + src/query/useAccountSequenceNumber.ts | 8 +- src/query/useCheckTxSignatures.ts | 4 + src/query/useEndpoint.ts | 27 +- src/query/useFriendBot.ts | 6 +- src/query/useLatestLedger.ts | 13 +- src/query/useLatestTxn.ts | 4 +- src/query/useSimulateTx.ts | 7 +- src/query/useSubmitHorizonTx.ts | 9 +- src/query/useSubmitRpcTx.ts | 15 +- src/store/createStore.ts | 12 +- src/types/types.ts | 10 +- tests/networkSelector.test.ts | 8 +- 24 files changed, 379 insertions(+), 227 deletions(-) create mode 100644 src/helpers/getNetworkHeaders.ts diff --git a/src/app/(sidebar)/account/create/page.tsx b/src/app/(sidebar)/account/create/page.tsx index ecd4f53f..4e48a63a 100644 --- a/src/app/(sidebar)/account/create/page.tsx +++ b/src/app/(sidebar)/account/create/page.tsx @@ -9,6 +9,7 @@ import { useFriendBot } from "@/query/useFriendBot"; import { useQueryClient } from "@tanstack/react-query"; import { useIsTestingNetwork } from "@/hooks/useIsTestingNetwork"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { GenerateKeypair } from "@/components/GenerateKeypair"; import { ExpandBox } from "@/components/ExpandBox"; @@ -49,6 +50,7 @@ export default function CreateAccount() { network, publicKey: account.publicKey!, key: { type: "create" }, + headers: getNetworkHeaders(network, "horizon"), }); useEffect(() => { diff --git a/src/app/(sidebar)/account/fund/page.tsx b/src/app/(sidebar)/account/fund/page.tsx index ea6604c4..e78a6df2 100644 --- a/src/app/(sidebar)/account/fund/page.tsx +++ b/src/app/(sidebar)/account/fund/page.tsx @@ -6,6 +6,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { useFriendBot } from "@/query/useFriendBot"; import { useStore } from "@/store/useStore"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { validate } from "@/validate"; @@ -38,6 +39,7 @@ export default function FundAccount() { network, publicKey: generatedPublicKey, key: { type: "fund" }, + headers: getNetworkHeaders(network, "horizon"), }); const queryClient = useQueryClient(); diff --git a/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx b/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx index 5f8ec1f1..c9cd7018 100644 --- a/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx +++ b/src/app/(sidebar)/endpoints/[[...pages]]/page.tsx @@ -35,6 +35,7 @@ import { arrayItem } from "@/helpers/arrayItem"; import { delayedAction } from "@/helpers/delayedAction"; import { buildEndpointHref } from "@/helpers/buildEndpointHref"; import { shareableUrl } from "@/helpers/shareableUrl"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { Routes } from "@/constants/routes"; import { @@ -278,12 +279,11 @@ export default function Endpoints() { refetch, isSuccess, isError, - } = useEndpoint( + } = useEndpoint({ requestUrl, - // There is only one endpoint request for POST, using params directly for - // simplicity. - pageData?.requestMethod === "POST" ? getPostPayload() : undefined, - ); + postData: pageData?.requestMethod === "POST" ? getPostPayload() : undefined, + headers: getNetworkHeaders(network, isRpcEndpoint ? "rpc" : "horizon"), + }); const responseEl = useRef(null); diff --git a/src/app/(sidebar)/transaction/build/components/Params.tsx b/src/app/(sidebar)/transaction/build/components/Params.tsx index dd032cf7..35438bdf 100644 --- a/src/app/(sidebar)/transaction/build/components/Params.tsx +++ b/src/app/(sidebar)/transaction/build/components/Params.tsx @@ -25,6 +25,7 @@ import { useStore } from "@/store/useStore"; import { useAccountSequenceNumber } from "@/query/useAccountSequenceNumber"; import { validate } from "@/validate"; import { EmptyObj, KeysOfUnion } from "@/types/types"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; export const Params = () => { const requiredParams = ["source_account", "seq_num", "fee"] as const; @@ -63,6 +64,7 @@ export const Params = () => { } = useAccountSequenceNumber({ publicKey: txnParams.source_account, horizonUrl: network.horizonUrl, + headers: getNetworkHeaders(network, "horizon"), }); // Preserve values and validate inputs when components mounts diff --git a/src/app/(sidebar)/transaction/simulate/page.tsx b/src/app/(sidebar)/transaction/simulate/page.tsx index 712fcbf3..8e54b59a 100644 --- a/src/app/(sidebar)/transaction/simulate/page.tsx +++ b/src/app/(sidebar)/transaction/simulate/page.tsx @@ -10,6 +10,7 @@ import { PrettyJson } from "@/components/PrettyJson"; import { useStore } from "@/store/useStore"; import { useSimulateTx } from "@/query/useSimulateTx"; import { delayedAction } from "@/helpers/delayedAction"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { validate } from "@/validate"; export default function SimulateTransaction() { @@ -75,6 +76,7 @@ export default function SimulateTransaction() { rpcUrl: network.rpcUrl, transactionXdr: xdr.blob, instructionLeeway: simulate.instructionLeeway, + headers: getNetworkHeaders(network, "rpc"), }); if (simulate.triggerOnLaunch) { diff --git a/src/app/(sidebar)/transaction/submit/page.tsx b/src/app/(sidebar)/transaction/submit/page.tsx index ebb03739..c4079e04 100644 --- a/src/app/(sidebar)/transaction/submit/page.tsx +++ b/src/app/(sidebar)/transaction/submit/page.tsx @@ -16,6 +16,7 @@ import * as StellarXdr from "@/helpers/StellarXdr"; import { delayedAction } from "@/helpers/delayedAction"; import { openUrl } from "@/helpers/openUrl"; import { getBlockExplorerLink } from "@/helpers/getBlockExplorerLink"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { localStorageSubmitMethod } from "@/helpers/localStorageSubmitMethod"; import { buildEndpointHref } from "@/helpers/buildEndpointHref"; @@ -73,7 +74,9 @@ export default function SubmitTransaction() { const [isSaveTxnModalVisible, setIsSaveTxnModalVisible] = useState(false); const [isDropdownActive, setIsDropdownActive] = useState(false); const [isDropdownVisible, setIsDropdownVisible] = useState(false); - const [submitMethod, setSubmitMethod] = useState(""); + const [submitMethod, setSubmitMethod] = useState<"horizon" | "rpc" | string>( + "", + ); const dropdownRef = useRef(null); const responseSuccessEl = useRef(null); @@ -178,21 +181,30 @@ export default function SubmitTransaction() { }; const handleSubmit = () => { - if (submitMethod === "rpc") { - submitRpc({ - rpcUrl: network.rpcUrl, - transactionXdr: blob, - networkPassphrase: network.passphrase, - }); - } else if (submitMethod === "horizon") { - submitHorizon({ - horizonUrl: network.horizonUrl, - transactionXdr: blob, - networkPassphrase: network.passphrase, - }); - } else { - // Do nothing - } + resetSubmitState(); + + delayedAction({ + action: () => { + if (submitMethod === "rpc") { + submitRpc({ + rpcUrl: network.rpcUrl, + transactionXdr: blob, + networkPassphrase: network.passphrase, + headers: getNetworkHeaders(network, submitMethod), + }); + } else if (submitMethod === "horizon") { + submitHorizon({ + horizonUrl: network.horizonUrl, + transactionXdr: blob, + networkPassphrase: network.passphrase, + headers: getNetworkHeaders(network, submitMethod), + }); + } else { + // Do nothing + } + }, + delay: 300, + }); }; const onSimulateTx = () => { diff --git a/src/app/(sidebar)/xdr/view/page.tsx b/src/app/(sidebar)/xdr/view/page.tsx index c74eabcb..235b351a 100644 --- a/src/app/(sidebar)/xdr/view/page.tsx +++ b/src/app/(sidebar)/xdr/view/page.tsx @@ -29,6 +29,7 @@ import { parseToLosslessJson } from "@/helpers/parseToLosslessJson"; import { useIsXdrInit } from "@/hooks/useIsXdrInit"; import { useStore } from "@/store/useStore"; import { delayedAction } from "@/helpers/delayedAction"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; export default function ViewXdr() { const { xdr, network } = useStore(); @@ -43,7 +44,7 @@ export default function ViewXdr() { isFetching: isLatestTxnFetching, isLoading: isLatestTxnLoading, refetch: fetchLatestTxn, - } = useLatestTxn(network.horizonUrl); + } = useLatestTxn(network.horizonUrl, getNetworkHeaders(network, "horizon")); const queryClient = useQueryClient(); diff --git a/src/components/FormElements/LedgerSeqPicker.tsx b/src/components/FormElements/LedgerSeqPicker.tsx index 738fa1c9..ec3b2717 100644 --- a/src/components/FormElements/LedgerSeqPicker.tsx +++ b/src/components/FormElements/LedgerSeqPicker.tsx @@ -6,6 +6,7 @@ import { PositiveIntPicker } from "@/components/FormElements/PositiveIntPicker"; import { useLatestLedger } from "@/query/useLatestLedger"; import { useStore } from "@/store/useStore"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; interface LedgerSeqPickerProps { id: string; @@ -37,6 +38,7 @@ export const LedgerSeqPicker = ({ isLoading, } = useLatestLedger({ rpcUrl: network.rpcUrl, + headers: getNetworkHeaders(network, "rpc"), }); useEffect(() => { diff --git a/src/components/NetworkSelector/index.tsx b/src/components/NetworkSelector/index.tsx index 3492f353..b8325425 100644 --- a/src/components/NetworkSelector/index.tsx +++ b/src/components/NetworkSelector/index.tsx @@ -8,11 +8,15 @@ import React, { import { Button, Icon, Input, Notification } from "@stellar/design-system"; import { NetworkIndicator } from "@/components/NetworkIndicator"; -import { localStorageSavedNetwork } from "@/helpers/localStorageSavedNetwork"; -import { delayedAction } from "@/helpers/delayedAction"; import { NetworkOptions } from "@/constants/settings"; import { useStore } from "@/store/useStore"; -import { Network, NetworkType } from "@/types/types"; + +import { localStorageSavedNetwork } from "@/helpers/localStorageSavedNetwork"; +import { delayedAction } from "@/helpers/delayedAction"; +import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { sanitizeObject } from "@/helpers/sanitizeObject"; + +import { AnyObject, EmptyObj, Network, NetworkType } from "@/types/types"; import "./styles.scss"; @@ -25,46 +29,28 @@ export const NetworkSelector = () => { updateIsDynamicNetworkSelect, } = useStore(); - const [activeNetworkId, setActiveNetworkId] = useState(network.id); - const [isDropdownActive, setIsDropdownActive] = useState(false); - const [isDropdownVisible, setIsDropdownVisible] = useState(false); - const { updateNetwork } = endpoints; - const initialCustomState = { - horizonUrl: network.id === "custom" ? network.horizonUrl : "", - rpcUrl: network.id === "custom" ? network.rpcUrl : "", - passphrase: network.id === "custom" ? network.passphrase : "", - }; + const [activeNetwork, setActiveNetwork] = useState( + network, + ); + const [validationError, setValidationError] = useState({}); + + const [isDropdownActive, setIsDropdownActive] = useState(false); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); - const [customNetwork, setCustomNetwork] = useState(initialCustomState); - const [mainnetRpc, setMainnetRpc] = useState(""); const buttonRef = useRef(null); const dropdownRef = useRef(null); - const isSameNetwork = () => { - if (activeNetworkId === "custom") { - return ( - network.horizonUrl && - network.rpcUrl && - network.passphrase && - customNetwork.horizonUrl === network.horizonUrl && - customNetwork.rpcUrl === network.rpcUrl && - customNetwork.passphrase === network.passphrase - ); - } - - if (activeNetworkId === "mainnet") { - return ( - network.horizonUrl && - network.rpcUrl && - network.passphrase && - mainnetRpc === network.rpcUrl - ); - } - - return activeNetworkId === network.id; - }; + const isSameNetwork = + activeNetwork.id === network.id && + activeNetwork.horizonUrl === network.horizonUrl && + activeNetwork.horizonHeaderName === network.horizonHeaderName && + activeNetwork.horizonHeaderValue === network.horizonHeaderValue && + activeNetwork.rpcUrl === network.rpcUrl && + activeNetwork.rpcHeaderName === network.rpcHeaderName && + activeNetwork.rpcHeaderValue === network.rpcHeaderValue && + activeNetwork.passphrase === network.passphrase; const isNetworkUrlInvalid = (url: string) => { if (!url) { @@ -79,47 +65,57 @@ export const NetworkSelector = () => { } }; - const isSubmitDisabled = - isSameNetwork() || - // custom network - (activeNetworkId === "custom" && - !(customNetwork.horizonUrl && customNetwork.passphrase)) || - Boolean( - customNetwork.horizonUrl && isNetworkUrlInvalid(customNetwork.horizonUrl), - ) || - // mainnet ; - Boolean( - activeNetworkId === "mainnet" && - Boolean(mainnetRpc && isNetworkUrlInvalid(mainnetRpc)), - ); - - const isCustomNetwork = activeNetworkId === "custom"; - const isMainnetNetwork = activeNetworkId === "mainnet"; - - const setNetwork = useCallback(() => { - if (!network?.id) { - const defaultNetwork = - localStorageSavedNetwork.get() || getNetworkById("testnet"); - - if (defaultNetwork) { - selectNetwork(defaultNetwork); - setActiveNetworkId(defaultNetwork.id); - } + const isSubmitDisabled = () => { + if (isSameNetwork) { + return true; } - if (network.id === "mainnet") { - setMainnetRpc(network?.rpcUrl || ""); - } - }, [network.id, network?.rpcUrl, selectNetwork]); + return !(activeNetwork.horizonUrl && activeNetwork.passphrase); + }; // Set default network on launch useEffect(() => { - setNetwork(); - }, [setNetwork]); + let defaultNetwork: Network | undefined; + + if (network.id) { + const savedNetwork = localStorageSavedNetwork.get(); + + defaultNetwork = { ...(network as Network) }; + + // Get API keys from local storage if it's the same network + if ( + savedNetwork && + savedNetwork.id === network.id && + savedNetwork.passphrase === network.passphrase && + savedNetwork.horizonUrl === network.horizonUrl && + savedNetwork.rpcUrl === network.rpcUrl + ) { + defaultNetwork = { + ...defaultNetwork, + horizonHeaderName: savedNetwork.horizonHeaderName || "", + rpcHeaderName: savedNetwork.rpcHeaderName || "", + }; + } + } else { + defaultNetwork = + localStorageSavedNetwork.get() || getNetworkById("testnet"); + } + + if (defaultNetwork) { + setActiveNetwork(defaultNetwork); + + if (!network.id) { + selectNetwork(defaultNetwork); + updateNetwork(defaultNetwork); + } + } + // Not including network to avoid unnecessary re-renders + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { if (isDynamicNetworkSelect) { - setActiveNetworkId(network.id); + setActiveNetwork(network); localStorageSavedNetwork.set(network as Network); } // Not including network @@ -150,14 +146,9 @@ export const NetworkSelector = () => { } toggleDropdown(false); - setActiveNetworkId(network.id); - setCustomNetwork({ - horizonUrl: network.horizonUrl ?? "", - rpcUrl: network.rpcUrl ?? "", - passphrase: network.passphrase ?? "", - }); + setActiveNetwork(network); }, - [network.id, network.horizonUrl, network.rpcUrl, network.passphrase], + [network], ); // Close dropdown when clicked outside @@ -181,38 +172,48 @@ export const NetworkSelector = () => { ) => { event.preventDefault(); - const networkData = getNetworkById(activeNetworkId); - - if (networkData) { - const getData = () => { - if (isCustomNetwork) { - return { ...networkData, ...customNetwork }; - } - if (isMainnetNetwork) { - return { ...networkData, rpcUrl: mainnetRpc }; - } - return networkData; + const network = isEmptyObject(activeNetwork) + ? null + : (activeNetwork as Network); + + if (network) { + // Update store (header values won't persist) + selectNetwork(network); + // Also update the network setting for endpoints + updateNetwork(network); + // Update local state + setActiveNetwork(network); + + // Don't save header values in local storage + const savedNetwork: Network = { + ...network, + horizonHeaderValue: "", + rpcHeaderValue: "", }; + // Update local storage + localStorageSavedNetwork.set(sanitizeObject(savedNetwork)); - const latestData = getData(); + // Close dropdown + toggleDropdown(false); + updateIsDynamicNetworkSelect(false); + } + }; - selectNetwork(latestData); + const handleInputChange = (param: string, value: string | undefined) => { + const _network = { ...activeNetwork } as Network; - // also update the network setting for endpoints - updateNetwork(latestData); + setActiveNetwork({ ..._network, [param]: value }); - setCustomNetwork( - networkData.id === "custom" ? customNetwork : initialCustomState, - ); - localStorageSavedNetwork.set(latestData); - toggleDropdown(false); - updateIsDynamicNetworkSelect(false); + if (["rpcUrl", "horizonUrl"].includes(param)) { + validateInputUrl(param, value); } }; - const handleSelectActive = (networkId: NetworkType) => { - setActiveNetworkId(networkId); - setCustomNetwork(initialCustomState); + const validateInputUrl = (param: string, value: string | undefined) => { + setValidationError({ + ...validationError, + [param]: value ? isNetworkUrlInvalid(value) : false, + }); }; const getNetworkById = (networkId: NetworkType) => { @@ -220,11 +221,11 @@ export const NetworkSelector = () => { }; const getButtonLabel = () => { - if (activeNetworkId === "custom") { + if (activeNetwork.id === "custom") { return "Switch to Custom Network"; } - return `Switch to ${getNetworkById(activeNetworkId)?.label}`; + return `Switch to ${getNetworkById(activeNetwork.id)?.label}`; }; const toggleDropdown = (show: boolean) => { @@ -249,23 +250,6 @@ export const NetworkSelector = () => { } }; - const getRpcValue = () => { - if (isCustomNetwork) { - return customNetwork.rpcUrl; - } - if (isMainnetNetwork) { - return mainnetRpc; - } - return getNetworkById(activeNetworkId)?.rpcUrl; - }; - - const horizonValue = isCustomNetwork - ? customNetwork.horizonUrl - : getNetworkById(activeNetworkId)?.horizonUrl; - const passphraseValue = isCustomNetwork - ? customNetwork.passphrase - : getNetworkById(activeNetworkId)?.passphrase; - return (
); }; + +type NetworkInputProps = { + id: string; + label?: string; + placeholder?: string; + value: string | undefined; + onChange: (event: React.ChangeEvent) => void; + error?: React.ReactNode; + disabled?: boolean; + disableAutocomplete?: boolean; +}; + +const NetworkInput = ({ + id, + label, + placeholder, + value, + onChange, + error, + disabled, + disableAutocomplete, +}: NetworkInputProps) => { + return ( + + ); +}; diff --git a/src/components/PrettyJsonTransaction.tsx b/src/components/PrettyJsonTransaction.tsx index 94c122c5..4fcf894f 100644 --- a/src/components/PrettyJsonTransaction.tsx +++ b/src/components/PrettyJsonTransaction.tsx @@ -5,6 +5,7 @@ import { PrettyJson } from "@/components/PrettyJson"; import { signatureHint } from "@/helpers/signatureHint"; import { xdrUtils } from "@/helpers/xdr/utils"; import { formatAmount } from "@/helpers/formatAmount"; +import { getNetworkHeaders } from "@/helpers/getNetworkHeaders"; import { useCheckTxSignatures } from "@/query/useCheckTxSignatures"; import { useStore } from "@/store/useStore"; @@ -25,6 +26,7 @@ export const PrettyJsonTransaction = ({ xdr, networkPassphrase: network.passphrase, networkUrl: network.horizonUrl, + headers: getNetworkHeaders(network, "horizon"), }); const isTx = Boolean(json?.tx || json?.tx_fee_bump); diff --git a/src/helpers/fetchTxSignatures.ts b/src/helpers/fetchTxSignatures.ts index f35e642e..6474843a 100644 --- a/src/helpers/fetchTxSignatures.ts +++ b/src/helpers/fetchTxSignatures.ts @@ -1,3 +1,4 @@ +import { NetworkHeaders } from "@/types/types"; import { FeeBumpTransaction, hash, @@ -10,10 +11,12 @@ export const fetchTxSignatures = async ({ txXdr, networkUrl, networkPassphrase, + headers, }: { txXdr: string; networkUrl: string; networkPassphrase: string; + headers: NetworkHeaders; }) => { try { let tx = TransactionBuilder.fromXDR(txXdr, networkPassphrase); @@ -71,7 +74,9 @@ export const fetchTxSignatures = async ({ const srcAccount = accounts[i]; try { - const res = await fetch(`${networkUrl}/accounts/${srcAccount}`); + const res = await fetch(`${networkUrl}/accounts/${srcAccount}`, { + headers, + }); const resJson = await res.json(); if (sourceAccounts[srcAccount] && resJson?.signers) { diff --git a/src/helpers/getNetworkHeaders.ts b/src/helpers/getNetworkHeaders.ts new file mode 100644 index 00000000..9646c79e --- /dev/null +++ b/src/helpers/getNetworkHeaders.ts @@ -0,0 +1,16 @@ +import { EmptyObj, Network } from "@/types/types"; + +export const getNetworkHeaders = ( + network: Network | EmptyObj, + method: "horizon" | "rpc", +) => { + if (method === "rpc" && network.rpcHeaderName) { + return { [network.rpcHeaderName]: network.rpcHeaderValue || "" }; + } else if (method === "horizon" && network.horizonHeaderName) { + return { + [network.horizonHeaderName]: network.horizonHeaderValue || "", + }; + } + + return {}; +}; diff --git a/src/query/useAccountSequenceNumber.ts b/src/query/useAccountSequenceNumber.ts index c7bba05c..4caf2a3b 100644 --- a/src/query/useAccountSequenceNumber.ts +++ b/src/query/useAccountSequenceNumber.ts @@ -1,12 +1,15 @@ import { MuxedAccount, StrKey } from "@stellar/stellar-sdk"; import { useQuery } from "@tanstack/react-query"; +import { NetworkHeaders } from "@/types/types"; export const useAccountSequenceNumber = ({ publicKey, horizonUrl, + headers, }: { publicKey: string; horizonUrl: string; + headers: NetworkHeaders; }) => { const query = useQuery({ queryKey: ["useAccountSequenceNumber", { publicKey }], @@ -19,7 +22,10 @@ export const useAccountSequenceNumber = ({ } try { - const response = await fetch(`${horizonUrl}/accounts/${sourceAccount}`); + const response = await fetch( + `${horizonUrl}/accounts/${sourceAccount}`, + { headers }, + ); const responseJson = await response.json(); if (responseJson?.status === 0) { diff --git a/src/query/useCheckTxSignatures.ts b/src/query/useCheckTxSignatures.ts index 4007ab06..67fd627f 100644 --- a/src/query/useCheckTxSignatures.ts +++ b/src/query/useCheckTxSignatures.ts @@ -1,14 +1,17 @@ import { useQuery } from "@tanstack/react-query"; import { fetchTxSignatures } from "@/helpers/fetchTxSignatures"; +import { NetworkHeaders } from "@/types/types"; export const useCheckTxSignatures = ({ xdr, networkPassphrase, networkUrl, + headers, }: { xdr: string; networkPassphrase: string; networkUrl: string; + headers: NetworkHeaders; }) => { const query = useQuery({ queryKey: ["tx", "signatures"], @@ -18,6 +21,7 @@ export const useCheckTxSignatures = ({ txXdr: xdr, networkPassphrase, networkUrl, + headers, }); } catch (e) { throw new Error( diff --git a/src/query/useEndpoint.ts b/src/query/useEndpoint.ts index 7a743271..6588abd9 100644 --- a/src/query/useEndpoint.ts +++ b/src/query/useEndpoint.ts @@ -1,14 +1,22 @@ import { useQuery } from "@tanstack/react-query"; -import { AnyObject } from "@/types/types"; +import { AnyObject, NetworkHeaders } from "@/types/types"; -export const useEndpoint = (requestUrl: string, postData?: AnyObject) => { +export const useEndpoint = ({ + requestUrl, + postData, + headers, +}: { + requestUrl: string; + postData?: AnyObject; + headers: NetworkHeaders; +}) => { const query = useQuery({ queryKey: ["endpoint", "response", postData], queryFn: async () => { - const endpointResponse = await fetch( - requestUrl, - getPostOptions(postData), - ); + const endpointResponse = await fetch(requestUrl, { + headers, + ...getPostOptions(postData, headers), + }); const endpointResponseJson = await endpointResponse.json(); @@ -23,14 +31,17 @@ export const useEndpoint = (requestUrl: string, postData?: AnyObject) => { return query; }; -const getPostOptions = (postData: AnyObject | undefined) => { +const getPostOptions = ( + postData: AnyObject | undefined, + headers: NetworkHeaders, +) => { let newProps = {}; if (postData) { if (postData.jsonrpc) { // https://developers.stellar.org/docs/data/rpc newProps = { - headers: { "Content-Type": "application/json" }, + headers: { "Content-Type": "application/json", ...headers }, body: JSON.stringify(postData), }; } else { diff --git a/src/query/useFriendBot.ts b/src/query/useFriendBot.ts index 3c6aea9a..3a63ac1c 100644 --- a/src/query/useFriendBot.ts +++ b/src/query/useFriendBot.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { EmptyObj, Network } from "@/types/types"; +import { EmptyObj, Network, NetworkHeaders } from "@/types/types"; import { shortenStellarAddress } from "@/helpers/shortenStellarAddress"; @@ -7,10 +7,12 @@ export const useFriendBot = ({ network, publicKey, key, + headers, }: { network: Network | EmptyObj; publicKey: string; key: { type: string }; + headers: NetworkHeaders; }) => { const knownFriendbotURL = network.id === "futurenet" @@ -29,7 +31,7 @@ export const useFriendBot = ({ network.id === "custom" ? `${network.horizonUrl}/friendbot` : `${knownFriendbotURL}/`; - const response = await fetch(`${url}?addr=${publicKey}`); + const response = await fetch(`${url}?addr=${publicKey}`, { headers }); if (!response.ok) { const errorBody = await response.json(); diff --git a/src/query/useLatestLedger.ts b/src/query/useLatestLedger.ts index 78fa8158..7de11796 100644 --- a/src/query/useLatestLedger.ts +++ b/src/query/useLatestLedger.ts @@ -1,12 +1,19 @@ -import { SorobanRpc } from "@stellar/stellar-sdk"; +import { NetworkHeaders } from "@/types/types"; +import { rpc as StellarRpc } from "@stellar/stellar-sdk"; import { useQuery } from "@tanstack/react-query"; -export const useLatestLedger = ({ rpcUrl }: { rpcUrl: string }) => { +export const useLatestLedger = ({ + rpcUrl, + headers, +}: { + rpcUrl: string; + headers: NetworkHeaders; +}) => { const query = useQuery({ queryKey: ["useLatestLedger"], queryFn: async () => { - const rpcServer = new SorobanRpc.Server(rpcUrl); + const rpcServer = new StellarRpc.Server(rpcUrl, { headers }); try { const latestLedger = await rpcServer.getLatestLedger(); diff --git a/src/query/useLatestTxn.ts b/src/query/useLatestTxn.ts index 179ffc82..f02878a7 100644 --- a/src/query/useLatestTxn.ts +++ b/src/query/useLatestTxn.ts @@ -1,12 +1,14 @@ import { useQuery } from "@tanstack/react-query"; +import { NetworkHeaders } from "@/types/types"; -export const useLatestTxn = (horizonUrl: string) => { +export const useLatestTxn = (horizonUrl: string, headers: NetworkHeaders) => { const query = useQuery({ queryKey: ["xdr", "latestTxn"], queryFn: async () => { try { const request = await fetch( `${horizonUrl}/transactions?limit=1&order=desc`, + { headers }, ); const requestResponse = await request.json(); diff --git a/src/query/useSimulateTx.ts b/src/query/useSimulateTx.ts index 88754a63..42f64d9d 100644 --- a/src/query/useSimulateTx.ts +++ b/src/query/useSimulateTx.ts @@ -1,10 +1,11 @@ import { useMutation } from "@tanstack/react-query"; -import { AnyObject } from "@/types/types"; +import { AnyObject, NetworkHeaders } from "@/types/types"; type SimulateTxProps = { rpcUrl: string; transactionXdr: string; instructionLeeway?: string; + headers: NetworkHeaders; }; export const useSimulateTx = () => { @@ -13,11 +14,13 @@ export const useSimulateTx = () => { rpcUrl, transactionXdr, instructionLeeway, + headers, }: SimulateTxProps) => { const res = await fetch(rpcUrl, { method: "POST", headers: { "Content-Type": "application/json", + ...headers, }, body: JSON.stringify({ jsonrpc: "2.0", @@ -47,12 +50,14 @@ export const useSimulateTx = () => { rpcUrl, transactionXdr, instructionLeeway, + headers, }: SimulateTxProps) => { try { await mutation.mutateAsync({ rpcUrl, transactionXdr, instructionLeeway, + headers, }); } catch (e) { // do nothing diff --git a/src/query/useSubmitHorizonTx.ts b/src/query/useSubmitHorizonTx.ts index 44cb7dc7..4d8a94fa 100644 --- a/src/query/useSubmitHorizonTx.ts +++ b/src/query/useSubmitHorizonTx.ts @@ -1,11 +1,13 @@ import { useMutation } from "@tanstack/react-query"; import { Horizon, TransactionBuilder } from "@stellar/stellar-sdk"; -import { SubmitHorizonError } from "@/types/types"; +import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { NetworkHeaders, SubmitHorizonError } from "@/types/types"; type SubmitHorizonTxProps = { horizonUrl: string; transactionXdr: string; networkPassphrase: string; + headers: NetworkHeaders; }; export const useSubmitHorizonTx = () => { @@ -18,12 +20,15 @@ export const useSubmitHorizonTx = () => { horizonUrl, transactionXdr, networkPassphrase, + headers, }: SubmitHorizonTxProps) => { const transaction = TransactionBuilder.fromXDR( transactionXdr, networkPassphrase, ); - const horizonServer = new Horizon.Server(horizonUrl); + const horizonServer = new Horizon.Server(horizonUrl, { + headers: isEmptyObject(headers) ? undefined : { ...headers }, + }); return (await horizonServer.submitTransaction( transaction, )) as Horizon.HorizonApi.TransactionResponse; diff --git a/src/query/useSubmitRpcTx.ts b/src/query/useSubmitRpcTx.ts index 034b681c..8ea02a50 100644 --- a/src/query/useSubmitRpcTx.ts +++ b/src/query/useSubmitRpcTx.ts @@ -1,12 +1,18 @@ import { useMutation } from "@tanstack/react-query"; -import { SorobanRpc, TransactionBuilder } from "@stellar/stellar-sdk"; +import { rpc as StellarRpc, TransactionBuilder } from "@stellar/stellar-sdk"; import { delay } from "@/helpers/delay"; -import { SubmitRpcError, SubmitRpcResponse } from "@/types/types"; +import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { + NetworkHeaders, + SubmitRpcError, + SubmitRpcResponse, +} from "@/types/types"; type SubmitRpcTxProps = { rpcUrl: string; transactionXdr: string; networkPassphrase: string; + headers: NetworkHeaders; }; export const useSubmitRpcTx = () => { @@ -19,13 +25,16 @@ export const useSubmitRpcTx = () => { rpcUrl, transactionXdr, networkPassphrase, + headers, }: SubmitRpcTxProps) => { try { const transaction = TransactionBuilder.fromXDR( transactionXdr, networkPassphrase, ); - const rpcServer = new SorobanRpc.Server(rpcUrl); + const rpcServer = new StellarRpc.Server(rpcUrl, { + headers: isEmptyObject(headers) ? undefined : { ...headers }, + }); const sentTx = await rpcServer.sendTransaction(transaction); if (sentTx.status !== "PENDING") { diff --git a/src/store/createStore.ts b/src/store/createStore.ts index 8d823a85..6a1dc801 100644 --- a/src/store/createStore.ts +++ b/src/store/createStore.ts @@ -508,7 +508,17 @@ export const createStore = (options: CreateStoreOptions) => // Select what to save in query string select() { return { - network: true, + network: { + id: true, + label: true, + horizonUrl: true, + horizonHeaderName: true, + horizonHeaderValue: false, + rpcUrl: true, + rpcHeaderName: true, + rpcHeaderValue: false, + passphrase: true, + }, account: false, endpoints: { params: true, diff --git a/src/types/types.ts b/src/types/types.ts index 74c9322f..4925324f 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,5 +1,5 @@ import React from "react"; -import { NetworkError, SorobanRpc, xdr } from "@stellar/stellar-sdk"; +import { NetworkError, rpc as StellarRpc, xdr } from "@stellar/stellar-sdk"; import { TransactionBuildParams } from "@/store/createStore"; // ============================================================================= @@ -25,10 +25,16 @@ export type Network = { id: NetworkType; label: string; horizonUrl: string; + horizonHeaderName?: string; + horizonHeaderValue?: string; rpcUrl: string; + rpcHeaderName?: string; + rpcHeaderValue?: string; passphrase: string; }; +export type NetworkHeaders = Record; + export type StatusPageComponent = { [key: string]: any; id: string; @@ -187,7 +193,7 @@ export type SavedTransactionPage = "build" | "sign" | "simulate" | "submit"; export type SubmitRpcResponse = { hash: string; - result: SorobanRpc.Api.GetSuccessfulTransactionResponse; + result: StellarRpc.Api.GetSuccessfulTransactionResponse; operationCount: number; fee: string; }; diff --git a/tests/networkSelector.test.ts b/tests/networkSelector.test.ts index c17d6323..c44ccaf0 100644 --- a/tests/networkSelector.test.ts +++ b/tests/networkSelector.test.ts @@ -44,7 +44,7 @@ test.describe("Network selector", () => { .getByTestId("networkSelector-dropdown") .locator("#rpc-url"); await expect(rpcField).toHaveValue("https://soroban-testnet.stellar.org"); - await expect(rpcField).toBeDisabled(); + await expect(rpcField).toBeEnabled(); // Horizon URL const horizonUrlField = page @@ -53,7 +53,7 @@ test.describe("Network selector", () => { await expect(horizonUrlField).toHaveValue( "https://horizon-testnet.stellar.org", ); - await expect(horizonUrlField).toBeDisabled(); + await expect(horizonUrlField).toBeEnabled(); // Network Passphrase const networkPassphraseField = page @@ -88,7 +88,7 @@ test.describe("Network selector", () => { .getByTestId("networkSelector-dropdown") .locator("#rpc-url"); await expect(rpcField).toHaveValue("https://rpc-futurenet.stellar.org"); - await expect(rpcField).toBeDisabled(); + await expect(rpcField).toBeEnabled(); // Horizon URL const horizonUrlField = page @@ -97,7 +97,7 @@ test.describe("Network selector", () => { await expect(horizonUrlField).toHaveValue( "https://horizon-futurenet.stellar.org", ); - await expect(horizonUrlField).toBeDisabled(); + await expect(horizonUrlField).toBeEnabled(); // Network Passphrase const networkPassphraseField = page From 684ee3ecb176c0a6323a6c4d179f2c083d10c410 Mon Sep 17 00:00:00 2001 From: Iveta Date: Wed, 6 Nov 2024 10:49:39 -0500 Subject: [PATCH 6/9] Allow localhost network (#1104) * Network Selector UI updated * Update network requests * Fix tests * Allow localhost network * Update CSP --- src/middleware.ts | 3 ++- src/query/useLatestLedger.ts | 5 ++++- src/query/useSubmitHorizonTx.ts | 1 + src/query/useSubmitRpcTx.ts | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index 77eec511..b40cf2d1 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -4,6 +4,7 @@ export function middleware(request: NextRequest) { const nonce = Buffer.from(crypto.randomUUID()).toString("base64"); // script-src 'unsafe-eval' is needed for XDR JSON WebAssembly scripts + // connect-src http://localhost:* to allow local network const cspHeader = ` default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https: 'unsafe-inline' 'unsafe-eval'; @@ -13,7 +14,7 @@ export function middleware(request: NextRequest) { : `'unsafe-inline'` }; img-src 'self' https://stellar.creit.tech/wallet-icons/ blob: data:; - connect-src 'self' https:; + connect-src 'self' http://localhost:* https:; font-src 'self' https://fonts.gstatic.com/; object-src 'none'; frame-src 'self' https://connect.trezor.io/; diff --git a/src/query/useLatestLedger.ts b/src/query/useLatestLedger.ts index 7de11796..47f4d749 100644 --- a/src/query/useLatestLedger.ts +++ b/src/query/useLatestLedger.ts @@ -13,7 +13,10 @@ export const useLatestLedger = ({ const query = useQuery({ queryKey: ["useLatestLedger"], queryFn: async () => { - const rpcServer = new StellarRpc.Server(rpcUrl, { headers }); + const rpcServer = new StellarRpc.Server(rpcUrl, { + headers, + allowHttp: new URL(rpcUrl).hostname === "localhost", + }); try { const latestLedger = await rpcServer.getLatestLedger(); diff --git a/src/query/useSubmitHorizonTx.ts b/src/query/useSubmitHorizonTx.ts index 4d8a94fa..059bfa2e 100644 --- a/src/query/useSubmitHorizonTx.ts +++ b/src/query/useSubmitHorizonTx.ts @@ -28,6 +28,7 @@ export const useSubmitHorizonTx = () => { ); const horizonServer = new Horizon.Server(horizonUrl, { headers: isEmptyObject(headers) ? undefined : { ...headers }, + allowHttp: new URL(horizonUrl).hostname === "localhost", }); return (await horizonServer.submitTransaction( transaction, diff --git a/src/query/useSubmitRpcTx.ts b/src/query/useSubmitRpcTx.ts index 8ea02a50..1d67e230 100644 --- a/src/query/useSubmitRpcTx.ts +++ b/src/query/useSubmitRpcTx.ts @@ -34,6 +34,7 @@ export const useSubmitRpcTx = () => { ); const rpcServer = new StellarRpc.Server(rpcUrl, { headers: isEmptyObject(headers) ? undefined : { ...headers }, + allowHttp: new URL(rpcUrl).hostname === "localhost", }); const sentTx = await rpcServer.sendTransaction(transaction); From 46ccc52eb18bcad33a1ad310ddeae0ab6bd96fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jeesun=20=EC=A7=80=EC=84=A0?= Date: Wed, 6 Nov 2024 16:36:27 -0800 Subject: [PATCH 7/9] Add a muxed account doc to muxed account page (#1137) --- src/app/(sidebar)/account/muxed-create/page.tsx | 16 ++++++++++++---- src/app/(sidebar)/account/muxed-parse/page.tsx | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/app/(sidebar)/account/muxed-create/page.tsx b/src/app/(sidebar)/account/muxed-create/page.tsx index 4e015c13..550b25cf 100644 --- a/src/app/(sidebar)/account/muxed-create/page.tsx +++ b/src/app/(sidebar)/account/muxed-create/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert, Button, Card, Input, Text } from "@stellar/design-system"; +import { Alert, Button, Card, Input, Link, Text } from "@stellar/design-system"; import { useStore } from "@/store/useStore"; @@ -9,6 +9,7 @@ import { ExpandBox } from "@/components/ExpandBox"; import { MuxedAccountResult } from "@/components/MuxedAccountResult"; import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; import { SdsLink } from "@/components/SdsLink"; +import { WithInfoText } from "@/components/WithInfoText"; import { muxedAccount } from "@/helpers/muxedAccount"; @@ -68,9 +69,11 @@ export default function CreateMuxedAccount() {
- - Create Multiplexed Account - + + + Create Multiplexed Account + + A muxed (or multiplexed) account (defined in{" "} @@ -165,6 +168,11 @@ export default function CreateMuxedAccount() { title="Muxed accounts are uncommon" > Don’t use in a production environment unless you know what you’re doing. + Read more about Muxed accounts{" "} + + here + + . {Boolean(sdkError) && ( diff --git a/src/app/(sidebar)/account/muxed-parse/page.tsx b/src/app/(sidebar)/account/muxed-parse/page.tsx index 511eea6a..e9f8784c 100644 --- a/src/app/(sidebar)/account/muxed-parse/page.tsx +++ b/src/app/(sidebar)/account/muxed-parse/page.tsx @@ -1,13 +1,14 @@ "use client"; import { useState } from "react"; -import { Alert, Card, Text, Button } from "@stellar/design-system"; +import { Alert, Card, Link, Text, Button } from "@stellar/design-system"; import { useStore } from "@/store/useStore"; import { ExpandBox } from "@/components/ExpandBox"; import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; import { MuxedAccountResult } from "@/components/MuxedAccountResult"; +import { WithInfoText } from "@/components/WithInfoText"; import { muxedAccount } from "@/helpers/muxedAccount"; @@ -59,9 +60,11 @@ export default function ParseMuxedAccount() {
- - Get Muxed Account from M address - + + + Get Muxed Account from M address + +
Don’t use in a production environment unless you know what you’re doing. + Read more about Muxed accounts{" "} + + here + + . {Boolean(sdkError) && ( From fbd718df616716909757d78d4928242a77ad75b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jeesun=20=EC=A7=80=EC=84=A0?= Date: Thu, 7 Nov 2024 10:59:07 -0800 Subject: [PATCH 8/9] remove leadingZeros for and validate.getAmountError (#1142) --- src/app/(sidebar)/transaction/build/components/Params.tsx | 3 ++- src/app/(sidebar)/transaction/fee-bump/page.tsx | 3 ++- src/components/formComponentTemplateEndpoints.tsx | 8 +++++--- src/components/formComponentTemplateTxnOps.tsx | 6 ++++-- src/helpers/removeLeadingZeroes.ts | 2 ++ 5 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 src/helpers/removeLeadingZeroes.ts diff --git a/src/app/(sidebar)/transaction/build/components/Params.tsx b/src/app/(sidebar)/transaction/build/components/Params.tsx index 35438bdf..2cfe2acd 100644 --- a/src/app/(sidebar)/transaction/build/components/Params.tsx +++ b/src/app/(sidebar)/transaction/build/components/Params.tsx @@ -19,6 +19,7 @@ import { TimeBoundsPicker } from "@/components/FormElements/TimeBoundsPicker"; import { sanitizeObject } from "@/helpers/sanitizeObject"; import { isEmptyObject } from "@/helpers/isEmptyObject"; +import { removeLeadingZeroes } from "@/helpers/removeLeadingZeroes"; import { TransactionBuildParams } from "@/store/createStore"; import { useStore } from "@/store/useStore"; @@ -335,7 +336,7 @@ export const Params = () => { { const id = "fee"; diff --git a/src/app/(sidebar)/transaction/fee-bump/page.tsx b/src/app/(sidebar)/transaction/fee-bump/page.tsx index 2726a319..c70a73c3 100644 --- a/src/app/(sidebar)/transaction/fee-bump/page.tsx +++ b/src/app/(sidebar)/transaction/fee-bump/page.tsx @@ -12,6 +12,7 @@ import { validate } from "@/validate"; import { sanitizeObject } from "@/helpers/sanitizeObject"; import { txHelper, FeeBumpedTxResponse } from "@/helpers/txHelper"; +import { removeLeadingZeroes } from "@/helpers/removeLeadingZeroes"; import { Box } from "@/components/layout/Box"; import { PositiveIntPicker } from "@/components/FormElements/PositiveIntPicker"; @@ -202,7 +203,7 @@ export default function FeeBumpTransaction() { { const id = "fee"; diff --git a/src/components/formComponentTemplateEndpoints.tsx b/src/components/formComponentTemplateEndpoints.tsx index aaee4c5e..44aa1495 100644 --- a/src/components/formComponentTemplateEndpoints.tsx +++ b/src/components/formComponentTemplateEndpoints.tsx @@ -17,6 +17,8 @@ import { MultiLedgerEntriesPicker } from "@/components/FormElements/XdrLedgerKey import { ConfigSettingIdPicker } from "@/components/FormElements/ConfigSettingIdPicker"; import { parseJsonString } from "@/helpers/parseJsonString"; +import { removeLeadingZeroes } from "@/helpers/removeLeadingZeroes"; + import { validate } from "@/validate"; import { AnyObject, @@ -349,7 +351,7 @@ export const formComponentTemplateEndpoints = ( id={id} label="Destination Amount" labelSuffix={!templ.isRequired ? "optional" : undefined} - value={templ.value || ""} + value={templ.value ? removeLeadingZeroes(templ.value) : ""} error={templ.error} onChange={templ.onChange} /> @@ -698,7 +700,7 @@ export const formComponentTemplateEndpoints = ( id={id} label="Source Amount" labelSuffix={!templ.isRequired ? "optional" : undefined} - value={templ.value || ""} + value={templ.value ? removeLeadingZeroes(templ.value) : ""} error={templ.error} onChange={templ.onChange} /> @@ -761,7 +763,7 @@ export const formComponentTemplateEndpoints = ( id={id} label="Starting Balance" labelSuffix={!templ.isRequired ? "optional" : undefined} - value={templ.value || ""} + value={templ.value ? removeLeadingZeroes(templ.value) : ""} error={templ.error} onChange={templ.onChange} /> diff --git a/src/components/formComponentTemplateTxnOps.tsx b/src/components/formComponentTemplateTxnOps.tsx index 8dff0251..f3c48c8f 100644 --- a/src/components/formComponentTemplateTxnOps.tsx +++ b/src/components/formComponentTemplateTxnOps.tsx @@ -15,6 +15,8 @@ import { NumberFractionPicker } from "@/components/FormElements/NumberFractionPi import { RevokeSponsorshipPicker } from "@/components/FormElements/RevokeSponsorshipPicker"; import { ClaimantsPicker } from "@/components/FormElements/ClaimantsPicker"; +import { removeLeadingZeroes } from "@/helpers/removeLeadingZeroes"; + import { validate } from "@/validate"; import { AnyObject, @@ -124,7 +126,7 @@ export const formComponentTemplateTxnOps = ({ id={id} label={custom?.label || "Amount"} labelSuffix={!templ.isRequired ? "optional" : undefined} - value={templ.value || ""} + value={templ.value ? removeLeadingZeroes(templ.value) : ""} error={templ.error} onChange={templ.onChange} note={custom?.note} @@ -641,7 +643,7 @@ export const formComponentTemplateTxnOps = ({ id={id} label="Starting Balance" labelSuffix={!templ.isRequired ? "optional" : undefined} - value={templ.value || ""} + value={templ.value ? removeLeadingZeroes(templ.value) : ""} error={templ.error} onChange={templ.onChange} /> diff --git a/src/helpers/removeLeadingZeroes.ts b/src/helpers/removeLeadingZeroes.ts new file mode 100644 index 00000000..7c2af85d --- /dev/null +++ b/src/helpers/removeLeadingZeroes.ts @@ -0,0 +1,2 @@ +export const removeLeadingZeroes = (numStr: string) => + numStr.replace(/^0+/, "") || "0"; From a775d84e001052547c3d72d155504f8a8baab01f Mon Sep 17 00:00:00 2001 From: Iveta Date: Thu, 7 Nov 2024 15:02:53 -0500 Subject: [PATCH 9/9] Account: Saved Keypairs (#1138) * Saved Keypairs * Make keys read-only + mask secret * Match design other saved pages * Add warning --- src/app/(sidebar)/account/create/page.tsx | 89 +++++-- src/app/(sidebar)/account/saved/page.tsx | 238 ++++++++++++++++++ src/app/(sidebar)/account/styles.scss | 20 ++ .../components/SavedEndpointsPage.tsx | 17 +- src/app/(sidebar)/transaction/saved/page.tsx | 20 +- src/components/SaveKeypairModal.tsx | 150 +++++++++++ .../SavedItemTimestampAndDelete.tsx | 30 +++ src/constants/navItems.tsx | 10 + src/constants/routes.ts | 1 + src/constants/settings.ts | 1 + src/helpers/localStorageSavedKeypairs.ts | 22 ++ src/types/types.ts | 8 + 12 files changed, 558 insertions(+), 48 deletions(-) create mode 100644 src/app/(sidebar)/account/saved/page.tsx create mode 100644 src/components/SaveKeypairModal.tsx create mode 100644 src/components/SavedItemTimestampAndDelete.tsx create mode 100644 src/helpers/localStorageSavedKeypairs.ts diff --git a/src/app/(sidebar)/account/create/page.tsx b/src/app/(sidebar)/account/create/page.tsx index 4e48a63a..131f1d3a 100644 --- a/src/app/(sidebar)/account/create/page.tsx +++ b/src/app/(sidebar)/account/create/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Card, Text, Button } from "@stellar/design-system"; +import { Card, Text, Button, Icon } from "@stellar/design-system"; import { Keypair } from "@stellar/stellar-sdk"; import { useStore } from "@/store/useStore"; @@ -15,6 +15,8 @@ import { GenerateKeypair } from "@/components/GenerateKeypair"; import { ExpandBox } from "@/components/ExpandBox"; import { SuccessMsg } from "@/components/FriendBot/SuccessMsg"; import { ErrorMsg } from "@/components/FriendBot/ErrorMsg"; +import { Box } from "@/components/layout/Box"; +import { SaveKeypairModal } from "@/components/SaveKeypairModal"; import "../styles.scss"; @@ -24,6 +26,7 @@ export default function CreateAccount() { const [secretKey, setSecretKey] = useState(""); const [showAlert, setShowAlert] = useState(false); + const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); const queryClient = useQueryClient(); const IS_TESTING_NETWORK = useIsTestingNetwork(); @@ -97,32 +100,64 @@ export default function CreateAccount() { account signer, and/or as a stellar-core node key.
-
- - - {IS_TESTING_NETWORK || IS_CUSTOM_NETWORK_WITH_HORIZON ? ( - ) : null} -
+ + <> + {IS_TESTING_NETWORK ? ( + + ) : null} + + + + <> + {IS_TESTING_NETWORK || IS_CUSTOM_NETWORK_WITH_HORIZON ? ( + + ) : null} + + {Boolean(account.publicKey) && ( @@ -152,6 +187,16 @@ export default function CreateAccount() { setShowAlert(false); }} /> + + { + setIsSaveModalVisible(false); + }} + publicKey={account.publicKey!} + secretKey={secretKey} + />
); } diff --git a/src/app/(sidebar)/account/saved/page.tsx b/src/app/(sidebar)/account/saved/page.tsx new file mode 100644 index 00000000..5546087e --- /dev/null +++ b/src/app/(sidebar)/account/saved/page.tsx @@ -0,0 +1,238 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { Alert, Text, Card, Input, Icon, Button } from "@stellar/design-system"; + +import { Box } from "@/components/layout/Box"; +import { InputSideElement } from "@/components/InputSideElement"; +import { SaveKeypairModal } from "@/components/SaveKeypairModal"; +import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete"; + +import { useStore } from "@/store/useStore"; +import { localStorageSavedKeypairs } from "@/helpers/localStorageSavedKeypairs"; +import { NetworkOptions } from "@/constants/settings"; +import { useIsTestingNetwork } from "@/hooks/useIsTestingNetwork"; + +import { NetworkType, SavedKeypair } from "@/types/types"; +import { arrayItem } from "@/helpers/arrayItem"; + +export default function SavedKeypairs() { + const { network, selectNetwork, updateIsDynamicNetworkSelect } = useStore(); + + const [savedKeypairs, setSavedKeypairs] = useState([]); + const [currentKeypairTimestamp, setCurrentKeypairTimestamp] = useState< + number | undefined + >(); + + const IS_TESTING_NETWORK = useIsTestingNetwork(); + + const updateSavedKeypairs = useCallback(() => { + const keypairs = localStorageSavedKeypairs + .get() + .filter((s) => s.network.id === network.id); + setSavedKeypairs(keypairs); + }, [network.id]); + + useEffect(() => { + updateSavedKeypairs(); + }, [updateSavedKeypairs]); + + const SavedKeypair = ({ keypair }: { keypair: SavedKeypair }) => { + return ( + + { + setCurrentKeypairTimestamp(keypair.timestamp); + }} + icon={} + /> + } + /> + + + + + + + { + const savedKeypairs = localStorageSavedKeypairs.get(); + const indexToUpdate = savedKeypairs.findIndex( + (kp) => kp.timestamp === keypair.timestamp, + ); + + if (indexToUpdate >= 0) { + const updatedList = arrayItem.delete( + savedKeypairs, + indexToUpdate, + ); + + localStorageSavedKeypairs.set(updatedList); + updateSavedKeypairs(); + } + }} + /> + + + ); + }; + + const getNetworkById = (networkId: NetworkType) => { + const newNetwork = NetworkOptions.find((n) => n.id === networkId); + + if (newNetwork) { + updateIsDynamicNetworkSelect(true); + selectNetwork(newNetwork); + } + }; + + const renderOtherNetworkMessage = () => { + if (network.id === "testnet" || network.id === "futurenet") { + const otherNetworkLabel = + network.id === "testnet" ? "Futurenet" : "Testnet"; + + return ( + { + const newNetworkId = + network.id === "testnet" ? "futurenet" : "testnet"; + + getNetworkById(newNetworkId); + }} + > + {`You must switch your network to ${otherNetworkLabel} in order to see those saved + keypairs. This feature is only available on Futurenet and Testnet for + security reasons.`} + + ); + } + + return null; + }; + + const renderContent = () => { + if (IS_TESTING_NETWORK) { + return ( + + <> + {savedKeypairs.length === 0 + ? `There are no saved keypairs on ${network.label} network.` + : savedKeypairs.map((kp) => ( + + ))} + + + ); + } + + return ( + + + You must switch your network to Futurenet or Testnet in order to see + those saved keypairs. This feature is only available on Futurenet and + Testnet for security reasons. + + + + + + + + + ); + }; + + return ( + + +
+ + Saved Keypairs + +
+ + <> + {IS_TESTING_NETWORK ? ( + + Saved keypairs are stored in the browser’s localstorage + unencrypted and with no protection. Anyone using this browser will + be able to access the keys and the keys could be easily lost. Do + not save keys that are intended to hold or control value on + mainnet. Do not reuse keypairs from futurenet or testnet on + mainnet for security. For keys that will hold value, please + consider using a Stellar wallet. + + ) : null} + + + {renderContent()} +
+ + <>{renderOtherNetworkMessage()} + + { + setCurrentKeypairTimestamp(undefined); + + if (isUpdate) { + updateSavedKeypairs(); + } + }} + keypairTimestamp={currentKeypairTimestamp} + /> +
+ ); +} diff --git a/src/app/(sidebar)/account/styles.scss b/src/app/(sidebar)/account/styles.scss index 1cb11bb9..8dad6377 100644 --- a/src/app/(sidebar)/account/styles.scss +++ b/src/app/(sidebar)/account/styles.scss @@ -40,6 +40,26 @@ } } + &__create__buttons { + .Button { + min-width: fit-content; + } + + @media screen and (max-width: 420px) { + & > * { + width: 100%; + } + + &__group { + flex: 1; + + .Button { + flex: 1; + } + } + } + } + &__result { display: flex; flex-direction: column; diff --git a/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx b/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx index bec0c480..21b6c27a 100644 --- a/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx +++ b/src/app/(sidebar)/endpoints/components/SavedEndpointsPage.tsx @@ -16,6 +16,7 @@ import { InputSideElement } from "@/components/InputSideElement"; import { NextLink } from "@/components/NextLink"; import { ShareUrlButton } from "@/components/ShareUrlButton"; import { PrettyJsonTextarea } from "@/components/PrettyJsonTextarea"; +import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete"; import { NetworkOptions } from "@/constants/settings"; import { Routes } from "@/constants/routes"; @@ -300,17 +301,9 @@ export const SavedEndpointsPage = () => { - {`Last saved ${formatTimestamp(e.timestamp)}`} - - + /> {expandedPayloadIndex[idx] ? ( diff --git a/src/app/(sidebar)/transaction/saved/page.tsx b/src/app/(sidebar)/transaction/saved/page.tsx index 58b0fc73..5d3d6a54 100644 --- a/src/app/(sidebar)/transaction/saved/page.tsx +++ b/src/app/(sidebar)/transaction/saved/page.tsx @@ -9,15 +9,15 @@ import { Box } from "@/components/layout/Box"; import { Routes } from "@/constants/routes"; import { InputSideElement } from "@/components/InputSideElement"; import { SaveTransactionModal } from "@/components/SaveTransactionModal"; +import { ShareUrlButton } from "@/components/ShareUrlButton"; +import { SavedItemTimestampAndDelete } from "@/components/SavedItemTimestampAndDelete"; import { TRANSACTION_OPERATIONS } from "@/constants/transactionOperations"; -import { formatTimestamp } from "@/helpers/formatTimestamp"; import { useStore } from "@/store/useStore"; import { localStorageSavedTransactions } from "@/helpers/localStorageSavedTransactions"; import { arrayItem } from "@/helpers/arrayItem"; import { SavedTransaction, SavedTransactionPage } from "@/types/types"; -import { ShareUrlButton } from "@/components/ShareUrlButton"; export default function SavedTransactions() { const { network, transaction, xdr } = useStore(); @@ -161,17 +161,9 @@ export default function SavedTransactions() { - {`Last saved ${formatTimestamp(txn.timestamp)}`} - - + /> diff --git a/src/components/SaveKeypairModal.tsx b/src/components/SaveKeypairModal.tsx new file mode 100644 index 00000000..394ec407 --- /dev/null +++ b/src/components/SaveKeypairModal.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from "react"; +import { Button, Input, Modal } from "@stellar/design-system"; +import { arrayItem } from "@/helpers/arrayItem"; +import { localStorageSavedKeypairs } from "@/helpers/localStorageSavedKeypairs"; +import { getSaveItemNetwork } from "@/helpers/getSaveItemNetwork"; +import { useStore } from "@/store/useStore"; + +type SaveKeypairModalProps = ( + | { + type: "save"; + keypairTimestamp?: undefined; + publicKey: string; + secretKey: string; + } + | { + type: "editName"; + keypairTimestamp: number | undefined; + publicKey?: undefined; + secretKey?: undefined; + } +) & { + isVisible: boolean; + onClose: (isUpdate?: boolean) => void; +}; + +export const SaveKeypairModal = ({ + type, + publicKey, + secretKey, + isVisible, + keypairTimestamp, + onClose, +}: SaveKeypairModalProps) => { + const { network } = useStore(); + const [savedKeypairName, setSavedKeypairName] = useState(""); + + const allKeypairs = localStorageSavedKeypairs.get(); + const currentKeypair = allKeypairs.find( + (kp) => kp.timestamp === keypairTimestamp, + ); + const currentKeypairIndex = allKeypairs.findIndex( + (kp) => kp.timestamp === keypairTimestamp, + ); + const currentKeypairName = currentKeypair?.name || ""; + + useEffect(() => { + if (isVisible && type === "editName") { + setSavedKeypairName(currentKeypairName); + } + }, [isVisible, type, currentKeypairName]); + + const handleClose = (isUpdate?: boolean) => { + setSavedKeypairName(""); + onClose(isUpdate); + }; + + if (type === "editName" && keypairTimestamp !== undefined) { + return ( + + Edit Saved Keypair +
+ { + setSavedKeypairName(e.target.value); + }} + /> +
+ + + + +
+ ); + } + + if (type === "save") { + return ( + + Save Keypair +
+ { + setSavedKeypairName(e.target.value); + }} + /> +
+ + + + +
+ ); + } + + return null; +}; diff --git a/src/components/SavedItemTimestampAndDelete.tsx b/src/components/SavedItemTimestampAndDelete.tsx new file mode 100644 index 00000000..b1b42609 --- /dev/null +++ b/src/components/SavedItemTimestampAndDelete.tsx @@ -0,0 +1,30 @@ +import { Button, Icon, Text } from "@stellar/design-system"; +import { Box } from "@/components/layout/Box"; +import { formatTimestamp } from "@/helpers/formatTimestamp"; + +export const SavedItemTimestampAndDelete = ({ + timestamp, + onDelete, +}: { + timestamp: number; + onDelete: () => void; +}) => { + return ( + <> + + {`Last saved ${formatTimestamp(timestamp)}`} + + + + + ); +}; diff --git a/src/constants/navItems.tsx b/src/constants/navItems.tsx index 357cbb79..81794293 100644 --- a/src/constants/navItems.tsx +++ b/src/constants/navItems.tsx @@ -7,6 +7,16 @@ import { } from "@/constants/endpointsPages"; export const ACCOUNT_NAV_ITEMS = [ + { + navItems: [ + { + route: Routes.SAVED_KEYPAIRS, + label: "Saved Keypairs", + icon: , + }, + ], + hasBottomDivider: true, + }, { navItems: [ { diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 3cac90b8..19a854cd 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -7,6 +7,7 @@ export enum Routes { ACCOUNT_FUND = "/account/fund", ACCOUNT_CREATE_MUXED = "/account/muxed-create", ACCOUNT_PARSE_MUXED = "/account/muxed-parse", + SAVED_KEYPAIRS = "/account/saved", // Endpoints ENDPOINTS = "/endpoints", ENDPOINTS_SAVED = "/endpoints/saved", diff --git a/src/constants/settings.ts b/src/constants/settings.ts index af887f84..b46aea29 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -9,6 +9,7 @@ export const LOCAL_STORAGE_SAVED_RPC_METHODS = "stellar_lab_saved_rpc_endpoints"; export const LOCAL_STORAGE_SAVED_TRANSACTIONS = "stellar_lab_saved_transactions"; +export const LOCAL_STORAGE_SAVED_KEYPAIRS = "stellar_lab_saved_keypairs"; export const LOCAL_STORAGE_SAVED_THEME = "stellarTheme:Laboratory"; export const XDR_TYPE_TRANSACTION_ENVELOPE = "TransactionEnvelope"; diff --git a/src/helpers/localStorageSavedKeypairs.ts b/src/helpers/localStorageSavedKeypairs.ts new file mode 100644 index 00000000..fb28b858 --- /dev/null +++ b/src/helpers/localStorageSavedKeypairs.ts @@ -0,0 +1,22 @@ +import { LOCAL_STORAGE_SAVED_KEYPAIRS } from "@/constants/settings"; +import { SavedKeypair } from "@/types/types"; + +export const localStorageSavedKeypairs = { + get: () => { + const savedKeypairsString = localStorage.getItem( + LOCAL_STORAGE_SAVED_KEYPAIRS, + ); + return savedKeypairsString + ? (JSON.parse(savedKeypairsString) as SavedKeypair[]) + : []; + }, + set: (savedKeypairs: SavedKeypair[]) => { + return localStorage.setItem( + LOCAL_STORAGE_SAVED_KEYPAIRS, + JSON.stringify(savedKeypairs), + ); + }, + remove: () => { + return localStorage.removeItem(LOCAL_STORAGE_SAVED_KEYPAIRS); + }, +}; diff --git a/src/types/types.ts b/src/types/types.ts index 4925324f..8d297300 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -78,6 +78,14 @@ export type MuxedAccountFieldType = MuxedAccount & { error: string; }; +export type SavedKeypair = { + timestamp: number; + network: LocalStorageSavedNetwork; + name: string; + publicKey: string; + secretKey: string; +}; + // ============================================================================= // Asset // =============================================================================