diff --git a/src/App.tsx b/src/App.tsx index 1b4c350..1c58bea 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { getInitialWalletManagerState } from "./store/reducers/WalletManagerReducer"; import { getInitialWalletsState } from "./store/reducers/WalletsReducer"; import { getInitialSettingsState } from "./store/reducers/SettingsReducer"; +import { getInitialNodeState } from "./store/reducers/NodeReducer"; // Set up localisation import "./utils/i18n"; @@ -25,7 +26,8 @@ { walletManager: getInitialWalletManagerState(), wallets: getInitialWalletsState(), - settings: getInitialSettingsState() + settings: getInitialSettingsState(), + node: getInitialNodeState() }, devToolsEnhancer({}) ); diff --git a/src/components/wallets/SyncWallets.tsx b/src/components/wallets/SyncWallets.tsx index b614076..fd0d45b 100644 --- a/src/components/wallets/SyncWallets.tsx +++ b/src/components/wallets/SyncWallets.tsx @@ -1,19 +1,22 @@ -import { useMountEffect } from "../../utils"; +import { useEffect } from "react"; -import { useDispatch, useSelector, shallowEqual } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../../store"; import { syncWallets } from "../../krist/wallets/Wallet"; /** Sync the wallets with the Krist node on startup. */ export function SyncWallets(): JSX.Element | null { - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useSelector((s: RootState) => s.wallets); + const syncNode = useSelector((s: RootState) => s.node.syncNode); const dispatch = useDispatch(); - useMountEffect(() => { + // 'wallets' is not a dependency here, we don't want to re-fetch on every + // wallet update, just the initial startup + useEffect(() => { // TODO: show errors to the user? - syncWallets(dispatch, wallets).catch(console.error); - }); + syncWallets(dispatch, syncNode, wallets).catch(console.error); + }, [syncNode]); return null; } diff --git a/src/components/ws/SyncWork.tsx b/src/components/ws/SyncWork.tsx index 1cddf52..a3628dd 100644 --- a/src/components/ws/SyncWork.tsx +++ b/src/components/ws/SyncWork.tsx @@ -7,14 +7,10 @@ import { AppDispatch } from "../../App"; import { APIResponse, KristWorkDetailed } from "../../krist/api/types"; -import packageJson from "../../../package.json"; - import Debug from "debug"; const debug = Debug("kristweb:sync-work"); -export async function updateDetailedWork(dispatch: AppDispatch): Promise { - const syncNode = packageJson.defaultSyncNode; // TODO: support alt nodes - +export async function updateDetailedWork(dispatch: AppDispatch, syncNode: string): Promise { debug("updating detailed work"); const res = await fetch(syncNode + "/work/detailed"); @@ -30,13 +26,13 @@ /** Sync the work with the Krist node on startup. */ export function SyncWork(): JSX.Element | null { - const lastBlockID = useSelector((s: RootState) => s.node.lastBlockID); + const { lastBlockID, syncNode } = useSelector((s: RootState) => s.node); const dispatch = useDispatch(); useEffect(() => { // TODO: show errors to the user? - updateDetailedWork(dispatch).catch(console.error); - }, [lastBlockID]); + updateDetailedWork(dispatch, syncNode).catch(console.error); + }, [lastBlockID, syncNode]); return null; } diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index 3063669..640bfbe 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -1,4 +1,4 @@ -import { useMemo, useEffect } from "react"; +import { useState, useEffect } from "react"; import { useSelector, shallowEqual, useDispatch } from "react-redux"; import { AppDispatch } from "../../App"; @@ -7,8 +7,6 @@ import * as wsActions from "../../store/actions/WebsocketActions"; import * as nodeActions from "../../store/actions/NodeActions"; -import packageJson from "../../../package.json"; - import { APIResponse, KristAddress, KristBlock, KristTransaction, WSConnectionState, WSIncomingMessage, WSSubscriptionLevel } from "../../krist/api/types"; import { findWalletByAddress, syncWalletUpdate } from "../../krist/wallets/Wallet"; import WebSocketAsPromised from "websocket-as-promised"; @@ -16,7 +14,6 @@ import throttle from "lodash.throttle"; import Debug from "debug"; -import { useMountEffect } from "../../utils"; const debug = Debug("kristweb:ws"); const REFRESH_THROTTLE_MS = 500; @@ -36,7 +33,7 @@ // TODO: automatically clean this up? private refreshThrottles: Record void> = {}; - constructor(private dispatch: AppDispatch) { + constructor(private dispatch: AppDispatch, private syncNode: string) { debug("WS component init"); this.attemptConnect(); } @@ -49,10 +46,8 @@ debug("attempting connection to server..."); this.setConnectionState("disconnected"); - const syncNode = packageJson.defaultSyncNode; // TODO: support alt nodes - // Get a websocket token - const res = await fetch(syncNode + "/ws/start", { method: "POST" }); + const res = await fetch(this.syncNode + "/ws/start", { method: "POST" }); if (!res.ok || res.status !== 200) throw new Error("ws.errorToken"); const data: APIResponse<{ url: string }> = await res.json(); if (!data.ok || data.error) throw new Error("ws.errorToken"); @@ -219,17 +214,25 @@ export function WebsocketService(): JSX.Element | null { const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const syncNode = useSelector((s: RootState) => s.node.syncNode); const dispatch = useDispatch(); - const connection = useMemo(() => new WebsocketConnection(dispatch), []); - - useMountEffect(() => { - // On unmount, force close the existing connection - return () => connection.forceClose(); - }); + const [connection, setConnection] = useState(); useEffect(() => { - connection.setWallets(wallets); + debug("!!!!!! useMountEffect"); + if (connection) connection.forceClose(); + setConnection(new WebsocketConnection(dispatch, syncNode)); + + // On unmount, force close the existing connection + return () => { + debug("!!!!!! useMountEffect ended"); + if (connection) connection.forceClose(); + }; + }, [syncNode]); + + useEffect(() => { + if (connection) connection.setWallets(wallets); }, [wallets]); return null; diff --git a/src/krist/api/lookup.ts b/src/krist/api/lookup.ts index 8f1242e..130456c 100644 --- a/src/krist/api/lookup.ts +++ b/src/krist/api/lookup.ts @@ -1,7 +1,5 @@ import { APIResponse, KristAddress } from "./types"; -import packageJson from "../../../package.json"; - interface LookupAddressesResponse { found: number; notFound: number; @@ -11,11 +9,9 @@ export interface KristAddressWithNames extends KristAddress { names?: number } export type LookupResults = Record; -export async function lookupAddresses(addresses: string[], fetchNames?: boolean): Promise { +export async function lookupAddresses(syncNode: string, addresses: string[], fetchNames?: boolean): Promise { if (!addresses || addresses.length === 0) return {}; - const syncNode = packageJson.defaultSyncNode; // TODO: support alt nodes - try { const res = await fetch( syncNode diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index e7e69cc..f2afb2f 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -130,10 +130,10 @@ /** Sync the data for a single wallet from the sync node, save it to local * storage, and dispatch the change to the Redux store. */ -export async function syncWallet(dispatch: AppDispatch, wallet: Wallet): Promise { +export async function syncWallet(dispatch: AppDispatch, syncNode: string, wallet: Wallet): Promise { // Fetch the data from the sync node (e.g. balance) const { address } = wallet; - const lookupResults = await lookupAddresses([address], true); + const lookupResults = await lookupAddresses(syncNode, [address], true); const kristAddress = lookupResults[address]; if (!kristAddress) return; // Skip unsyncable wallet @@ -156,12 +156,12 @@ /** Sync the data for all the wallets from the sync node, save it to local * storage, and dispatch the changes to the Redux store. */ -export async function syncWallets(dispatch: AppDispatch, wallets: WalletMap): Promise { +export async function syncWallets(dispatch: AppDispatch, syncNode: string, wallets: WalletMap): Promise { const syncTime = new Date(); // Fetch all the data from the sync node (e.g. balances) const addresses = Object.values(wallets).map(w => w.address); - const lookupResults = await lookupAddresses(addresses, true); + const lookupResults = await lookupAddresses(syncNode, addresses, true); // Create a WalletMap with the updated wallet properties const updatedWallets = Object.entries(wallets).map(([_, wallet]) => { @@ -183,6 +183,7 @@ * * @param dispatch - The AppDispatch instance used to dispatch the new wallet to * the Redux store. + * @param syncNode - The Krist sync node to fetch the wallet data from. * @param masterPassword - The master password used to encrypt the wallet * password and privatekey. * @param wallet - The information for the new wallet. @@ -191,6 +192,7 @@ */ export async function addWallet( dispatch: AppDispatch, + syncNode: string, masterPassword: string, wallet: WalletNew, password: string, @@ -226,7 +228,7 @@ // Dispatch the changes to the redux store dispatch(actions.addWallet(newWallet)); - syncWallet(dispatch, newWallet); + syncWallet(dispatch, syncNode, newWallet); } /** @@ -235,6 +237,7 @@ * * @param dispatch - The AppDispatch instance used to dispatch the new wallet to * the Redux store. + * @param syncNode - The Krist sync node to fetch the wallet data from. * @param masterPassword - The master password used to encrypt the wallet * password and privatekey. * @param wallet - The old wallet information. @@ -243,6 +246,7 @@ */ export async function editWallet( dispatch: AppDispatch, + syncNode: string, masterPassword: string, wallet: Wallet, updated: WalletNew, @@ -275,7 +279,7 @@ // Dispatch the changes to the redux store dispatch(actions.updateWallet(wallet.id, finalWallet)); - syncWallet(dispatch, finalWallet); + syncWallet(dispatch, syncNode, finalWallet); } /** Deletes a wallet, removing it from local storage and dispatching the change diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index 881ce27..1c173a2 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -50,6 +50,7 @@ const { masterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); // Required to check for existing wallets const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const syncNode = useSelector((s: RootState) => s.node.syncNode); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -99,7 +100,7 @@ }); } - await editWallet(dispatch, masterPassword, editing, values, values.password); + await editWallet(dispatch, syncNode, masterPassword, editing, values, values.password); message.success(t("addWallet.messageSuccessEdit")); closeModal(); @@ -120,7 +121,7 @@ }); } - await addWallet(dispatch, masterPassword, values, values.password, values.save ?? true); + await addWallet(dispatch, syncNode, masterPassword, values, values.password, values.save ?? true); message.success(create ? t("addWallet.messageSuccessCreate") : t("addWallet.messageSuccessAdd")); closeModal(); diff --git a/src/store/actions/NodeActions.ts b/src/store/actions/NodeActions.ts index 865f7ff..fc78f6c 100644 --- a/src/store/actions/NodeActions.ts +++ b/src/store/actions/NodeActions.ts @@ -3,5 +3,6 @@ import * as constants from "../constants"; +export const setSyncNode = createAction(constants.SYNC_NODE)(); export const setLastBlockID = createAction(constants.LAST_BLOCK_ID)(); export const setDetailedWork = createAction(constants.DETAILED_WORK)(); diff --git a/src/store/constants.ts b/src/store/constants.ts index 6f54c4a..8b3fbd2 100644 --- a/src/store/constants.ts +++ b/src/store/constants.ts @@ -21,5 +21,6 @@ export const CONNECTION_STATE = "CONNECTION_STATE"; // Node state +export const SYNC_NODE = "SYNC_NODE"; export const LAST_BLOCK_ID = "LAST_BLOCK_ID"; export const DETAILED_WORK = "DETAILED_WORK"; diff --git a/src/store/reducers/NodeReducer.ts b/src/store/reducers/NodeReducer.ts index a2d9249..9bab7c0 100644 --- a/src/store/reducers/NodeReducer.ts +++ b/src/store/reducers/NodeReducer.ts @@ -1,17 +1,27 @@ import { createReducer, ActionType } from "typesafe-actions"; import { KristWorkDetailed } from "../../krist/api/types"; -import { setLastBlockID, setDetailedWork } from "../actions/NodeActions"; +import { setSyncNode, setLastBlockID, setDetailedWork } from "../actions/NodeActions"; + +import packageJson from "../../../package.json"; export interface State { readonly lastBlockID: number; readonly detailedWork?: KristWorkDetailed; + readonly syncNode: string; } -export const initialState: State = { - lastBlockID: 0 -}; +export function getInitialNodeState(): State { + return { + lastBlockID: 0, + syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode + }; +} -export const NodeReducer = createReducer(initialState) +export const NodeReducer = createReducer({} as State) + .handleAction(setSyncNode, (state: State, action: ActionType) => ({ + ...state, + syncNode: action.payload + })) .handleAction(setLastBlockID, (state: State, action: ActionType) => ({ ...state, lastBlockID: action.payload diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 3965c38..2f35fbd 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -44,3 +44,14 @@ localStorage.setItem(getSettingKey(settingName), value ? "true" : "false"); dispatch(actions.setBooleanSetting(settingName, value)); } + +export function isValidSyncNode(syncNode?: string): boolean { + if (!syncNode) return false; + + try { + const url = new URL(syncNode); + return url.protocol === "http:" || url.protocol === "https:"; + } catch (_) { + return false; + } +}