diff --git a/.vscode/settings.json b/.vscode/settings.json index 43a153a..2fced71 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "KRISTWALLET", "KRISTWALLETEXTENSION", "Lngs", + "Mutex", "Popconfirm", "Sider", "Syncable", diff --git a/package.json b/package.json index 7477ebc..4edc3db 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.7.1", "antd": "^4.12.3", + "async-mutex": "^0.3.0", "base64-arraybuffer": "^0.2.0", "csv-stringify": "^5.6.1", "dayjs": "^1.10.4", @@ -28,7 +29,7 @@ "i18next": "^19.7.0", "i18next-browser-languagedetector": "^6.0.1", "i18next-http-backend": "^1.0.20", - "lodash.throttle": "^4.1.1", + "lodash-es": "^4.17.21", "react": "^17.0.1", "react-dom": "^17.0.1", "react-i18next": "^11.8.6", @@ -71,7 +72,7 @@ "@types/debug": "^4.1.5", "@types/file-saver": "^2.0.1", "@types/jest": "^26.0.20", - "@types/lodash.throttle": "^4.1.6", + "@types/lodash-es": "^4.17.4", "@types/node": "^12.19.16", "@types/react": "^17.0.1", "@types/react-dom": "^17.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b47f8e6..e34048c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ '@testing-library/react': 11.2.5_react-dom@17.0.1+react@17.0.1 '@testing-library/user-event': 12.7.1 antd: 4.12.3_89622fd8e4ec221151a62783d49305af + async-mutex: 0.3.0 base64-arraybuffer: 0.2.0 csv-stringify: 5.6.1 dayjs: 1.10.4 @@ -12,7 +13,7 @@ i18next: 19.8.7 i18next-browser-languagedetector: 6.0.1 i18next-http-backend: 1.1.0 - lodash.throttle: 4.1.1 + lodash-es: 4.17.21 react: 17.0.1 react-dom: 17.0.1_react@17.0.1 react-i18next: 11.8.6_i18next@19.8.7+react@17.0.1 @@ -32,7 +33,7 @@ '@types/debug': 4.1.5 '@types/file-saver': 2.0.1 '@types/jest': 26.0.20 - '@types/lodash.throttle': 4.1.6 + '@types/lodash-es': 4.17.4 '@types/node': 12.20.0 '@types/react': 17.0.2 '@types/react-dom': 17.0.1 @@ -2083,12 +2084,12 @@ dev: true resolution: integrity: sha1-7ihweulOEdK4J7y+UnC86n8+ce4= - /@types/lodash.throttle/4.1.6: + /@types/lodash-es/4.17.4: dependencies: '@types/lodash': 4.14.168 dev: true resolution: - integrity: sha512-/UIH96i/sIRYGC60NoY72jGkCJtFN5KVPhEMMMTjol65effe1gPn0tycJqV5tlSwMTzX8FqzB5yAj0rfGHTPNg== + integrity: sha512-BBz79DCJbD2CVYZH67MBeHZRX++HF+5p8Mo5MzjZi64Wac39S3diedJYHZtScbRVf4DjZyN6LzA0SB0zy+HSSQ== /@types/lodash/4.14.168: dev: true resolution: @@ -2950,6 +2951,12 @@ dev: true resolution: integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + /async-mutex/0.3.0: + dependencies: + tslib: 2.1.0 + dev: false + resolution: + integrity: sha512-6VIpUM7s37EMXvnO3TvujgaS6gx4yJby13BhxovMYSap7nrbS0gJ1UzGcjD+HElNSdTz/+IlAIqj7H48N0ZlyQ== /async-validator/3.5.1: dev: false resolution: @@ -7890,6 +7897,10 @@ node: '>=8' resolution: integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + /lodash-es/4.17.21: + dev: false + resolution: + integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== /lodash._reinterpolate/3.0.0: dev: true resolution: @@ -7915,10 +7926,6 @@ dev: true resolution: integrity: sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - /lodash.throttle/4.1.1: - dev: false - resolution: - integrity: sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= /lodash.uniq/4.5.0: dev: true resolution: @@ -12524,7 +12531,6 @@ resolution: integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== /tslib/2.1.0: - dev: true resolution: integrity: sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== /tsutils/3.20.0_typescript@4.1.5: @@ -13542,7 +13548,7 @@ '@types/debug': ^4.1.5 '@types/file-saver': ^2.0.1 '@types/jest': ^26.0.20 - '@types/lodash.throttle': ^4.1.6 + '@types/lodash-es': ^4.17.4 '@types/node': ^12.19.16 '@types/react': ^17.0.1 '@types/react-dom': ^17.0.0 @@ -13555,6 +13561,7 @@ '@typescript-eslint/eslint-plugin': ^4.15.0 '@typescript-eslint/parser': ^4.15.0 antd: ^4.12.3 + async-mutex: ^0.3.0 base64-arraybuffer: ^0.2.0 craco-less: ^1.17.1 csv-stringify: ^5.6.1 @@ -13568,7 +13575,7 @@ i18next: ^19.7.0 i18next-browser-languagedetector: ^6.0.1 i18next-http-backend: ^1.0.20 - lodash.throttle: ^4.1.1 + lodash-es: ^4.17.21 prettier: ^2.2.1 react: ^17.0.1 react-dom: ^17.0.1 diff --git a/src/App.tsx b/src/App.tsx index 1c58bea..6f0a3bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import { ForcedAuth } from "./components/auth/ForcedAuth"; import { WebsocketService } from "./components/ws/WebsocketService"; import { SyncWork } from "./components/ws/SyncWork"; +import { SyncMOTD } from "./components/ws/SyncMOTD"; export const store = createStore( rootReducer, @@ -42,6 +43,7 @@ {/* Services, etc. */} + diff --git a/src/components/ContextualAddress.tsx b/src/components/ContextualAddress.tsx index 260941b..bd6573b 100644 --- a/src/components/ContextualAddress.tsx +++ b/src/components/ContextualAddress.tsx @@ -1,12 +1,15 @@ import React from "react"; import { Tooltip } from "antd"; +import { useSelector } from "react-redux"; +import { RootState } from "../store"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { KristAddress } from "../krist/api/types"; import { Wallet } from "../krist/wallets/Wallet"; import { parseCommonMeta, CommonMeta } from "../utils/commonmeta"; +import { stripNameSuffix } from "../utils/currency"; import { KristName } from "./KristName"; @@ -21,19 +24,18 @@ } interface AddressMetanameProps { + nameSuffix: string; address: string; commonMeta: CommonMeta; source: boolean; hideNameAddress: boolean; } -export function AddressMetaname({ address, commonMeta, source, hideNameAddress }: AddressMetanameProps): JSX.Element { +export function AddressMetaname({ nameSuffix, address, commonMeta, source, hideNameAddress }: AddressMetanameProps): JSX.Element { const rawMetaname = (source ? commonMeta?.return : commonMeta?.recipient) || undefined; const metaname = (source ? commonMeta?.returnMetaname : commonMeta?.metaname) || undefined; const name = (source ? commonMeta?.returnName : commonMeta?.name) || undefined; - - // TODO: support custom suffixes - const nameWithoutSuffix = name ? name.replace(/\.kst$/, "") : undefined; + const nameWithoutSuffix = name ? stripNameSuffix(nameSuffix, name) : undefined; return name ? <> @@ -60,13 +62,14 @@ export function ContextualAddress({ address: origAddress, wallet, metadata, source, hideNameAddress }: Props): JSX.Element { const { t } = useTranslation(); + const nameSuffix = useSelector((s: RootState) => s.node.currency.name_suffix); if (!origAddress) return ( {t("contextualAddressUnknown")} ); const address = typeof origAddress === "object" ? origAddress.address : origAddress; - const commonMeta = parseCommonMeta(metadata); + const commonMeta = parseCommonMeta(nameSuffix, metadata); const hasMetaname = source ? !!commonMeta?.returnRecipient : !!commonMeta?.recipient; return @@ -74,6 +77,7 @@ ? ( // Display the metaname and link to the name if possible s.node.currency.name_suffix); + return - {/* TODO: support other name suffixes */} - {name}.kst + {name}.{nameSuffix} ; } diff --git a/src/components/KristValue.tsx b/src/components/KristValue.tsx index b9527b6..cdaf960 100644 --- a/src/components/KristValue.tsx +++ b/src/components/KristValue.tsx @@ -1,5 +1,8 @@ import React from "react"; +import { useSelector } from "react-redux"; +import { RootState } from "../store"; + import { KristSymbol } from "./KristSymbol"; import "./KristValue.less"; @@ -13,13 +16,16 @@ type Props = React.HTMLProps & OwnProps; -export const KristValue = ({ value, long, hideNullish, green, ...props }: Props): JSX.Element | null => - hideNullish && (value === undefined || value === null) - ? null - : ( - - - {(value || 0).toLocaleString()} - {long && KST} - - ); +export const KristValue = ({ value, long, hideNullish, green, ...props }: Props): JSX.Element | null => { + const currencySymbol = useSelector((s: RootState) => s.node.currency.currency_symbol); + + if (hideNullish && (value === undefined || value === null)) return null; + + return ( + + {(currencySymbol || "KST") === "KST" && } + {(value || 0).toLocaleString()} + {long && {currencySymbol || "KST"}} + + ); +}; diff --git a/src/components/ws/SyncMOTD.tsx b/src/components/ws/SyncMOTD.tsx new file mode 100644 index 0000000..a6e6bd2 --- /dev/null +++ b/src/components/ws/SyncMOTD.tsx @@ -0,0 +1,62 @@ +import { useEffect } from "react"; + +import { useDispatch, useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; +import * as nodeActions from "../../store/actions/NodeActions"; + +import { AppDispatch } from "../../App"; +import { APIResponse, KristMOTD } from "../../krist/api/types"; + +import { recalculateWallets } from "../../krist/wallets/Wallet"; + +import Debug from "debug"; +const debug = Debug("kristweb:sync-motd"); + +export async function updateMOTD(dispatch: AppDispatch, syncNode: string): Promise { + debug("updating motd"); + + const res = await fetch(syncNode + "/motd"); + if (!res.ok || res.status !== 200) // TODO: handle API errors + throw new Error("error fetching motd"); + + const data: APIResponse = await res.json(); + if (!data?.ok) throw new Error("error fetching motd"); + + debug("motd: %s", data.motd); + dispatch(nodeActions.setCurrency(data.currency)); +} + +/** Sync the MOTD with the Krist node on startup. */ +export function SyncMOTD(): JSX.Element | null { + const syncNode = useSelector((s: RootState) => s.node.syncNode); + const connectionState = useSelector((s: RootState) => s.websocket.connectionState); + + // All these are used to determine if we need to recalculate the addresses + const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix); + const masterPassword = useSelector((s: RootState) => s.walletManager.masterPassword); + const wallets = useSelector((s: RootState) => s.wallets.wallets, shallowEqual); + + const dispatch = useDispatch(); + + // Update the MOTD when the sync node changes, and on startup + useEffect(() => { + // TODO: show errors to the user? + updateMOTD(dispatch, syncNode).catch(console.error); + }, [syncNode]); + + // Update the MOTD when the sync node reconnects, in case it changes in + // realtime (basically only used for development) + useEffect(() => { + if (connectionState !== "connected") return; + updateMOTD(dispatch, syncNode).catch(console.error); + }, [connectionState]); + + // When the currency's address prefix changes, or our master password appears, + // recalculate the addresses if necessary + useEffect(() => { + if (!addressPrefix || !masterPassword) return; + recalculateWallets(dispatch, masterPassword, wallets, addressPrefix).catch(console.error); + }, [addressPrefix, masterPassword, wallets]); + + return null; +} diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index a7271eb..a920a9a 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -11,7 +11,7 @@ import { findWalletByAddress, syncWallet, syncWalletUpdate } from "../../krist/wallets/Wallet"; import WebSocketAsPromised from "websocket-as-promised"; -import throttle from "lodash.throttle"; +import { throttle } from "lodash-es"; import Debug from "debug"; const debug = Debug("kristweb:ws"); diff --git a/src/krist/AddressAlgo.ts b/src/krist/AddressAlgo.ts index 0775d0b..dfbb5c1 100644 --- a/src/krist/AddressAlgo.ts +++ b/src/krist/AddressAlgo.ts @@ -5,9 +5,9 @@ return String.fromCharCode(byte + 39 > 122 ? 101 : byte > 57 ? byte + 39 : byte); }; -export const makeV2Address = async (key: string): Promise => { +export const makeV2Address = async (addressPrefix: string, key: string): Promise => { const chars = ["", "", "", "", "", "", "", "", ""]; - let chain = "k"; // TODO: custom prefixes + let chain = addressPrefix; let hash = await doubleSHA256(key); for (let i = 0; i <= 8; i++) { diff --git a/src/krist/api/types.ts b/src/krist/api/types.ts index da112e8..9c2603a 100644 --- a/src/krist/api/types.ts +++ b/src/krist/api/types.ts @@ -44,6 +44,38 @@ }; } +export interface KristCurrency { + address_prefix: string; + name_suffix: string; + currency_name: string; + currency_symbol: string; +} +export const DEFAULT_CURRENCY: KristCurrency = { + address_prefix: "k", name_suffix: "kst", + currency_name: "Krist", currency_symbol: "KST" +}; + +export interface KristMOTD { + motd: string; + set: string; + + public_url: string; + mining_enabled: boolean; + debug_enabled: boolean; + + constants: { + wallet_version: number; + nonce_max_size: number; + name_cost: number; + min_work: number; + max_work: number; + work_factor: number; + seconds_per_block: number; + }; + + currency: KristCurrency; +} + export type APIResponse> = T & { ok: boolean; error?: string; diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index f2afb2f..46ae46d 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -11,6 +11,11 @@ import * as actions from "../../store/actions/WalletsActions"; import { WalletMap } from "../../store/reducers/WalletsReducer"; +import { Mutex } from "async-mutex"; + +import Debug from "debug"; +const debug = Debug("kristweb:wallet"); + export interface Wallet { // UUID for this wallet id: string; @@ -184,6 +189,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 addressPrefix - The prefixes of addresses on this node. * @param masterPassword - The master password used to encrypt the wallet * password and privatekey. * @param wallet - The information for the new wallet. @@ -193,6 +199,7 @@ export async function addWallet( dispatch: AppDispatch, syncNode: string, + addressPrefix: string, masterPassword: string, wallet: WalletNew, password: string, @@ -200,7 +207,7 @@ ): Promise { // Calculate the privatekey for the given wallet format const privatekey = await applyWalletFormat(wallet.format || "kristwallet", password, wallet.username); - const address = await makeV2Address(privatekey); + const address = await makeV2Address(addressPrefix, privatekey); const id = uuid(); @@ -238,6 +245,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 addressPrefix - The prefixes of addresses on this node. * @param masterPassword - The master password used to encrypt the wallet * password and privatekey. * @param wallet - The old wallet information. @@ -247,6 +255,7 @@ export async function editWallet( dispatch: AppDispatch, syncNode: string, + addressPrefix: string, masterPassword: string, wallet: Wallet, updated: WalletNew, @@ -254,7 +263,7 @@ ): Promise { // Calculate the privatekey for the given wallet format const privatekey = await applyWalletFormat(updated.format || "kristwallet", password, updated.username); - const address = await makeV2Address(privatekey); + const address = await makeV2Address(addressPrefix, privatekey); // Encrypt the password and privatekey. These will be decrypted on-demand. const encPassword = await aesGcmEncrypt(password, masterPassword); @@ -317,3 +326,60 @@ return null; } + +const recalculationMutex = new Mutex(); +/** If the address prefix changes (e.g. swapping sync node), and we are + * decrypted, recalculate all the addresses with the new prefix. If the prefix + * is unchanged, this does nothing. The changes will be dispatched to the + * Redux store. */ +export async function recalculateWallets(dispatch: AppDispatch, masterPassword: string, wallets: WalletMap, addressPrefix: string): Promise { + const lastPrefix = localStorage.getItem("lastAddressPrefix") || "k"; + if (addressPrefix === lastPrefix) return; + debug("address prefix changed from %s to %s, waiting for mutex...", lastPrefix, addressPrefix); + + // Don't allow more than one recalculation at a time + await recalculationMutex.runExclusive(async () => { + const lastPrefix = localStorage.getItem("lastAddressPrefix") || "k"; + if (addressPrefix === lastPrefix) { + debug("prefix was already reset while we were calculating"); + return; + } + + debug("recalculating all wallets", lastPrefix, addressPrefix); + + // Map of wallet IDs -> new addresses + const updatedWallets: Record = {}; + + // Recalculate all the wallets + for (const id in wallets) { + // Prepare the wallet for recalculation + const wallet = wallets[id]; + const decrypted = await decryptWallet(masterPassword, wallet); + if (!decrypted) + throw new Error(`couldn't decrypt wallet ${wallet.id}!`); + + // Calculate the new address + const privatekey = await applyWalletFormat(wallet.format || "kristwallet", decrypted.password, wallet.username); + const address = await makeV2Address(addressPrefix, privatekey); + + if (wallet.address === address) continue; + + // Prepare the change to be applied + debug("old address: %s - new address: %s", wallet.address, address); + updatedWallets[wallet.id] = address; + } + + // Now that we know everything converted successfully, save the updated + // wallets to local storage + for (const id in wallets) { + const wallet = wallets[id]; + saveWallet({ ...wallet, address: updatedWallets[wallet.id] }); + } + + // Apply all the changes to the Redux store + dispatch(actions.recalculateWallets(updatedWallets)); + + debug("recalculation done, saving prefix"); + localStorage.setItem("lastAddressPrefix", addressPrefix); + }); +} diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index 1c173a2..3bc3997 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -51,6 +51,7 @@ // Required to check for existing wallets const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); const syncNode = useSelector((s: RootState) => s.node.syncNode); + const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix); const dispatch = useDispatch(); const { t } = useTranslation(); @@ -100,7 +101,7 @@ }); } - await editWallet(dispatch, syncNode, masterPassword, editing, values, values.password); + await editWallet(dispatch, syncNode, addressPrefix, masterPassword, editing, values, values.password); message.success(t("addWallet.messageSuccessEdit")); closeModal(); @@ -121,7 +122,7 @@ }); } - await addWallet(dispatch, syncNode, masterPassword, values, values.password, values.save ?? true); + await addWallet(dispatch, syncNode, addressPrefix, masterPassword, values, values.password, values.save ?? true); message.success(create ? t("addWallet.messageSuccessCreate") : t("addWallet.messageSuccessAdd")); closeModal(); @@ -147,7 +148,7 @@ /** Update the 'Wallet address' field */ async function updateCalculatedAddress(format: WalletFormatName | undefined, password: string, username?: string) { const privatekey = await applyWalletFormat(format || "kristwallet", password, username); - const address = await makeV2Address(privatekey); + const address = await makeV2Address(addressPrefix, privatekey); setCalculatedAddress(address); } diff --git a/src/store/actions/NodeActions.ts b/src/store/actions/NodeActions.ts index fc78f6c..b36aa6a 100644 --- a/src/store/actions/NodeActions.ts +++ b/src/store/actions/NodeActions.ts @@ -1,8 +1,9 @@ import { createAction } from "typesafe-actions"; -import { KristWorkDetailed } from "../../krist/api/types"; +import { KristWorkDetailed, KristCurrency } from "../../krist/api/types"; 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)(); +export const setCurrency = createAction(constants.DETAILED_WORK)(); diff --git a/src/store/actions/WalletsActions.ts b/src/store/actions/WalletsActions.ts index 93e8676..2bb47df 100644 --- a/src/store/actions/WalletsActions.ts +++ b/src/store/actions/WalletsActions.ts @@ -28,3 +28,7 @@ export interface SyncWalletsPayload { wallets: Record }; export const syncWallets = createAction(constants.SYNC_WALLETS, (wallets): SyncWalletsPayload => ({ wallets }))(); + +export interface RecalculateWalletsPayload { wallets: Record }; +export const recalculateWallets = createAction(constants.RECALCULATE_WALLETS, + (wallets): RecalculateWalletsPayload => ({ wallets }))(); diff --git a/src/store/constants.ts b/src/store/constants.ts index 8b3fbd2..04ae167 100644 --- a/src/store/constants.ts +++ b/src/store/constants.ts @@ -11,6 +11,7 @@ export const UPDATE_WALLET = "UPDATE_WALLET"; export const SYNC_WALLET = "SYNC_WALLET"; export const SYNC_WALLETS = "SYNC_WALLETS"; +export const RECALCULATE_WALLETS = "RECALCULATE_WALLETS"; // Settings // --- @@ -24,3 +25,4 @@ export const SYNC_NODE = "SYNC_NODE"; export const LAST_BLOCK_ID = "LAST_BLOCK_ID"; export const DETAILED_WORK = "DETAILED_WORK"; +export const CURRENCY = "CURRENCY"; diff --git a/src/store/reducers/NodeReducer.ts b/src/store/reducers/NodeReducer.ts index 9bab7c0..b7f956e 100644 --- a/src/store/reducers/NodeReducer.ts +++ b/src/store/reducers/NodeReducer.ts @@ -1,6 +1,6 @@ import { createReducer, ActionType } from "typesafe-actions"; -import { KristWorkDetailed } from "../../krist/api/types"; -import { setSyncNode, setLastBlockID, setDetailedWork } from "../actions/NodeActions"; +import { KristWorkDetailed, KristCurrency, DEFAULT_CURRENCY } from "../../krist/api/types"; +import { setSyncNode, setLastBlockID, setDetailedWork, setCurrency } from "../actions/NodeActions"; import packageJson from "../../../package.json"; @@ -8,12 +8,14 @@ readonly lastBlockID: number; readonly detailedWork?: KristWorkDetailed; readonly syncNode: string; + readonly currency: KristCurrency; } export function getInitialNodeState(): State { return { lastBlockID: 0, - syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode + syncNode: localStorage.getItem("syncNode") || packageJson.defaultSyncNode, + currency: DEFAULT_CURRENCY }; } @@ -29,5 +31,9 @@ .handleAction(setDetailedWork, (state: State, action: ActionType) => ({ ...state, detailedWork: action.payload + })) + .handleAction(setCurrency, (state: State, action: ActionType) => ({ + ...state, + currency: action.payload })); diff --git a/src/store/reducers/WalletsReducer.ts b/src/store/reducers/WalletsReducer.ts index 8747f7d..f53fb3a 100644 --- a/src/store/reducers/WalletsReducer.ts +++ b/src/store/reducers/WalletsReducer.ts @@ -75,4 +75,16 @@ .reduce((o, wallet) => ({ ...o, [wallet.id]: wallet }), {}); return { ...state, wallets: { ...state.wallets, ...updatedWallets }}; + }) + // Recalculate wallets + .handleAction(actions.recalculateWallets, (state: State, { payload }: ActionType) => { + const updatedWallets = Object.entries(payload.wallets) + .map(([id, newData]) => ({ // merge in the new data + ...(state.wallets[id]), // old data + address: newData // recalculated address + })) // convert back to a WalletMap + .reduce((o, wallet) => ({ ...o, [wallet.id]: wallet }), {}); + + return { ...state, wallets: { ...state.wallets, ...updatedWallets }}; }); + diff --git a/src/utils/commonmeta.ts b/src/utils/commonmeta.ts index a486cff..91c54c0 100644 --- a/src/utils/commonmeta.ts +++ b/src/utils/commonmeta.ts @@ -1,3 +1,5 @@ +import { getNameRegex } from "./currency"; + export interface CommonMeta { metaname?: string; name?: string; @@ -10,7 +12,7 @@ [key: string]: string | undefined; } -export function parseCommonMeta(metadata: string | undefined | null): CommonMeta | null { +export function parseCommonMeta(nameSuffix: string, metadata: string | undefined | null): CommonMeta | null { if (!metadata) return null; const parts: CommonMeta = {}; @@ -18,7 +20,7 @@ const metaParts = metadata.split(";"); if (metaParts.length <= 0) return null; - const nameMatches = /^(?:([a-z0-9-_]{1,32})@)?([a-z0-9]{1,64}\.kst)$/.exec(metaParts[0]); + const nameMatches = getNameRegex(nameSuffix).exec(metaParts[0]); if (nameMatches) { if (nameMatches[1]) parts.metaname = nameMatches[1]; if (nameMatches[2]) parts.name = nameMatches[2]; @@ -40,7 +42,7 @@ } if (parts.return) { - const returnMatches = /^(?:([a-z0-9-_]{1,32})@)?([a-z0-9]{1,64}\.kst)$/.exec(parts.return); + const returnMatches = getNameRegex(nameSuffix).exec(parts.return); if (returnMatches) { if (returnMatches[1]) parts.returnMetaname = returnMatches[1]; if (returnMatches[2]) parts.returnName = returnMatches[2]; diff --git a/src/utils/currency.ts b/src/utils/currency.ts new file mode 100644 index 0000000..d402048 --- /dev/null +++ b/src/utils/currency.ts @@ -0,0 +1,23 @@ +import { memoize, escapeRegExp, truncate, toString } from "lodash-es"; + +const _cleanNameSuffix = (nameSuffix: string | undefined | null): string => { + // Ensure the name suffix is safe to put into a RegExp + const stringSuffix = toString(nameSuffix); + const shortSuffix = truncate(stringSuffix, { length: MAX_NAME_SUFFIX_LENGTH, omission: "" }); + const escaped = escapeRegExp(shortSuffix); + return escaped; +}; +export const cleanNameSuffix = memoize(_cleanNameSuffix); + +// Cheap way to avoid RegExp DoS +const MAX_NAME_SUFFIX_LENGTH = 6; +const _getNameRegex = (nameSuffix: string | undefined | null): RegExp => + new RegExp(`^(?:([a-z0-9-_]{1,32})@)?([a-z0-9]{1,64}\\.${cleanNameSuffix(nameSuffix)})$`); +export const getNameRegex = memoize(_getNameRegex); + +const _stripNameSuffixRegExp = (nameSuffix: string | undefined | null): RegExp => + new RegExp(`\\.${cleanNameSuffix(nameSuffix)}$`); +export const stripNameSuffixRegExp = memoize(_stripNameSuffixRegExp); + +export const stripNameSuffix = (nameSuffix: string | undefined | null, inp: string): string => + inp.replace(stripNameSuffixRegExp(nameSuffix), "");