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